feat: Implement multi-user timetable tracking and notifications #1
6 changed files with 309 additions and 139 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ login.json
|
||||||
timetable*.json
|
timetable*.json
|
||||||
timetableFilled*.json
|
timetableFilled*.json
|
||||||
accounts.json
|
accounts.json
|
||||||
|
login*.json
|
||||||
|
|
|
||||||
106
Bot/Timetable.go
106
Bot/Timetable.go
|
|
@ -88,67 +88,73 @@ func ReadLoginResultFromFile(path string) (Loginresult, error) {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Timetable(cookies []*http.Cookie) {
|
func Timetable(cookies []*http.Cookie, userID string) {
|
||||||
loginResult, err := ReadLoginResultFromFile("login.json")
|
loginFile := "login.json"
|
||||||
if err != nil {
|
if userID != "" {
|
||||||
log.Fatal(err)
|
loginFile = "login_" + userID + ".json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginResult, err := ReadLoginResultFromFile(loginFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not read login result for user %s: %v", userID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
today := time.Now().Format("20060102")
|
today := time.Now().Format("20060102")
|
||||||
g := getTimetable{"2023-05-06 15:44:22.215292", "getTimetable", params{today, today, loginResult.PersonID, loginResult.PersonType}, "2.0"}
|
g := getTimetable{"2023-05-06 15:44:22.215292", "getTimetable", params{today, today, loginResult.PersonID, loginResult.PersonType}, "2.0"}
|
||||||
TimetablesJson, err := json.Marshal(g)
|
timetablesJson, err := json.Marshal(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error marshaling login data: %v", err)
|
log.Printf("Error marshaling timetable request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timetable := bytes.NewReader(TimetablesJson)
|
timetable := bytes.NewReader(timetablesJson)
|
||||||
|
|
||||||
prompt, err := http.NewRequest("GET", Url, timetable)
|
req, err := http.NewRequest("GET", Url, timetable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error creatingrequest: %v", err)
|
log.Printf("Error creating timetable request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//log.Println("prompt without extra header or cookie ", prompt)
|
req.Header.Set("Content-Type", "application/json")
|
||||||
//log.Println("Cookie: ", cookies)
|
|
||||||
|
|
||||||
prompt.Header.Set("Content-Type", "application/json")
|
|
||||||
prompt.Header.Set("User-Agent", "Webuntis Test")
|
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
//if cookie.Name == "JSESSIONID" {
|
req.AddCookie(cookie)
|
||||||
prompt.AddCookie(cookie)
|
|
||||||
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
//log.Println("Request JSON:", string(TeachersJson))
|
|
||||||
out, err := http.DefaultClient.Do(prompt)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during request: %v", err)
|
log.Printf("Error fetching timetable: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Body.Close()
|
defer resp.Body.Close()
|
||||||
//log.Println(out.Status)
|
|
||||||
response, err := io.ReadAll(out.Body)
|
response, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading response body: %v", err)
|
log.Printf("Error reading timetable response: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//responseString := string(response)
|
|
||||||
//log.Println("Repsonse ", responseString)
|
|
||||||
var Response TimetableResponse
|
var Response TimetableResponse
|
||||||
err = json.Unmarshal(response, &Response)
|
if err := json.Unmarshal(response, &Response); err != nil {
|
||||||
if err != nil {
|
log.Printf("Error unmarshaling timetable response: %v", err)
|
||||||
log.Fatalf("Error unmarshaling response: %v", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Printf("Error marshaling timetable result: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
err = os.WriteFile("timetable.json", data, 0644)
|
|
||||||
if err != nil {
|
timetableFile := "timetable.json"
|
||||||
log.Fatal(err)
|
if userID != "" {
|
||||||
|
timetableFile = "timetable_" + userID + ".json"
|
||||||
}
|
}
|
||||||
log.Println("Updated Timetable")
|
if err := os.WriteFile(timetableFile, data, 0644); err != nil {
|
||||||
setTimetable()
|
log.Printf("Error writing timetable file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Updated timetable for user %s", userID)
|
||||||
|
setTimetable(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadIDMap(path string) (map[int]string, error) {
|
func LoadIDMap(path string) (map[int]string, error) {
|
||||||
|
|
@ -189,14 +195,18 @@ func formatDate(date int) string {
|
||||||
day := s[6:8]
|
day := s[6:8]
|
||||||
return fmt.Sprintf("%s-%s-%s", day, month, year)
|
return fmt.Sprintf("%s-%s-%s", day, month, year)
|
||||||
}
|
}
|
||||||
func setTimetable() {
|
func setTimetable(userID string) {
|
||||||
subjects, _ := LoadIDMap("subjects.json")
|
subjects, _ := LoadIDMap("subjects.json")
|
||||||
rooms, _ := LoadIDMap("rooms.json")
|
rooms, _ := LoadIDMap("rooms.json")
|
||||||
classes, _ := LoadIDMap("classes.json")
|
classes, _ := LoadIDMap("classes.json")
|
||||||
timetable, _ := LoadTimetable("timetable.json")
|
|
||||||
|
timetableFile := "timetable.json"
|
||||||
|
if userID != "" {
|
||||||
|
timetableFile = "timetable_" + userID + ".json"
|
||||||
|
}
|
||||||
|
timetable, _ := LoadTimetable(timetableFile)
|
||||||
|
|
||||||
var namedTimetable []NamedTimetableEntry
|
var namedTimetable []NamedTimetableEntry
|
||||||
|
|
||||||
for _, lesson := range timetable {
|
for _, lesson := range timetable {
|
||||||
var klNames, suNames, roNames []string
|
var klNames, suNames, roNames []string
|
||||||
for _, kl := range lesson.Kl {
|
for _, kl := range lesson.Kl {
|
||||||
|
|
@ -224,11 +234,17 @@ func setTimetable() {
|
||||||
|
|
||||||
data, err := json.MarshalIndent(namedTimetable, "", " ")
|
data, err := json.MarshalIndent(namedTimetable, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Printf("Error marshaling named timetable: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
err = os.WriteFile("timetableFilled.json", data, 0644)
|
|
||||||
if err != nil {
|
timetableFilledFile := "timetableFilled.json"
|
||||||
log.Fatal(err)
|
if userID != "" {
|
||||||
|
timetableFilledFile = "timetableFilled_" + userID + ".json"
|
||||||
}
|
}
|
||||||
log.Println("Filled Timetable")
|
if err := os.WriteFile(timetableFilledFile, data, 0644); err != nil {
|
||||||
|
log.Printf("Error writing timetableFilled file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Filled timetable for user %s", userID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
Bot/Untis.go
49
Bot/Untis.go
|
|
@ -35,70 +35,59 @@ var Url = "https://thalia.webuntis.com/WebUntis/jsonrpc.do?school=Mons_Tabor"
|
||||||
//var Password = os.Getenv("UNTIS_PASSWORD")
|
//var Password = os.Getenv("UNTIS_PASSWORD")
|
||||||
//var USERS = os.Getenv("UNTIS_USER")
|
//var USERS = os.Getenv("UNTIS_USER")
|
||||||
|
|
||||||
func Main(user, password string) {
|
func Main(user, password, userID string) {
|
||||||
godotenv.Load("../.env")
|
godotenv.Load("../.env")
|
||||||
cookies, err := Auth(user, password)
|
cookies, err := Auth(user, password, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Printf("Authentication failed for user %s: %v", user, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Rooms(cookies)
|
Rooms(cookies)
|
||||||
|
|
||||||
Classes(cookies)
|
Classes(cookies)
|
||||||
|
|
||||||
Subjects(cookies)
|
Subjects(cookies)
|
||||||
|
Timetable(cookies, userID)
|
||||||
Timetable(cookies)
|
|
||||||
|
|
||||||
//getTeachers sends empty response
|
|
||||||
Teachers(cookies)
|
Teachers(cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Auth(user, password string) ([]*http.Cookie, error) {
|
func Auth(user, password, userID string) ([]*http.Cookie, error) {
|
||||||
l := Login{"2023-05-06 15:44:22.215292", "authenticate", Params{user, password, "WebUntis Test"}, "2.0"}
|
l := Login{"2023-05-06 15:44:22.215292", "authenticate", Params{user, password, "WebUntis Test"}, "2.0"}
|
||||||
loginJSON, err := json.Marshal(l)
|
loginJSON, err := json.Marshal(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error marshaling login data: %v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
login := bytes.NewReader(loginJSON)
|
login := bytes.NewReader(loginJSON)
|
||||||
|
|
||||||
//log.Println("Run")
|
|
||||||
LoginOut, err := http.Post(Url, "application/json", login)
|
LoginOut, err := http.Post(Url, "application/json", login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during authentication: %v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer LoginOut.Body.Close()
|
defer LoginOut.Body.Close()
|
||||||
|
|
||||||
// Parse cookies from response
|
|
||||||
cookies := LoginOut.Cookies()
|
cookies := LoginOut.Cookies()
|
||||||
//log.Println("Received cookies:", cookies)
|
log.Printf("Login successful for user: %s", user)
|
||||||
//log.Println("Set-Cookie headers:", LoginOut.Header["Set-Cookie"])
|
|
||||||
//loginRespBody, _ := io.ReadAll(LoginOut.Body)
|
|
||||||
//log.Println("Login response body:", string(loginRespBody))
|
|
||||||
|
|
||||||
log.Println("Login successful")
|
|
||||||
|
|
||||||
//log.Println("Login successful")
|
|
||||||
response, err := io.ReadAll(LoginOut.Body)
|
response, err := io.ReadAll(LoginOut.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error reading response body: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var Response LoginResponse
|
var Response LoginResponse
|
||||||
err = json.Unmarshal(response, &Response)
|
if err := json.Unmarshal(response, &Response); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
log.Fatalf("Error unmarshaling response: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
err = os.WriteFile("login.json", data, 0644)
|
|
||||||
if err != nil {
|
loginFile := "login.json"
|
||||||
log.Fatal(err)
|
if userID != "" {
|
||||||
|
loginFile = "login_" + userID + ".json"
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(loginFile, data, 0644); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cookies, nil
|
return cookies, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,35 @@ func Start() {
|
||||||
dg.Close()
|
dg.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecryptedAccount holds user information with a decrypted password.
|
||||||
|
type DecryptedAccount struct {
|
||||||
|
UserID string
|
||||||
|
Username string
|
||||||
|
DecryptedPassword string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAndDecryptAccounts loads all accounts from the JSON file and decrypts their passwords.
|
||||||
|
func LoadAndDecryptAccounts() ([]DecryptedAccount, error) {
|
||||||
|
accounts := loadAllAccounts()
|
||||||
|
decryptedAccounts := make([]DecryptedAccount, 0, len(accounts))
|
||||||
|
|
||||||
|
for _, acc := range accounts {
|
||||||
|
decPwd, err := decrypt(acc.Password)
|
||||||
|
if err != nil {
|
||||||
|
// Log the error but continue with other accounts
|
||||||
|
fmt.Printf("Error decrypting password for user %s: %v\n", acc.UserID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decryptedAccounts = append(decryptedAccounts, DecryptedAccount{
|
||||||
|
UserID: acc.UserID,
|
||||||
|
Username: acc.Username,
|
||||||
|
DecryptedPassword: decPwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedAccounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Expose this for main.go to trigger notifications
|
// Expose this for main.go to trigger notifications
|
||||||
func NotifyAllUsers() {
|
func NotifyAllUsers() {
|
||||||
if DiscordSession != nil {
|
if DiscordSession != nil {
|
||||||
|
|
@ -255,6 +284,77 @@ func NotifyAllUsers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendDM sends a direct message to a user.
|
||||||
|
func SendDM(userID, message string) {
|
||||||
|
if DiscordSession == nil {
|
||||||
|
fmt.Println("Discord session not initialized, cannot send DM.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Avoid sending messages to the "default" user placeholder
|
||||||
|
if userID == "default" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel, err := DiscordSession.UserChannelCreate(userID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating DM channel for %s: %v\n", userID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = DiscordSession.ChannelMessageSend(channel.ID, message)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error sending DM to %s: %v\n", userID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete account and associated files for a given userID
|
||||||
|
func deleteAccount(userID string) error {
|
||||||
|
var accounts []Account
|
||||||
|
|
||||||
|
// Load existing accounts
|
||||||
|
data, err := os.ReadFile(accountsFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // No accounts file means nothing to delete
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) > 0 {
|
||||||
|
if err := json.Unmarshal(data, &accounts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the account with matching userID
|
||||||
|
newAccounts := accounts[:0]
|
||||||
|
found := false
|
||||||
|
for _, acc := range accounts {
|
||||||
|
if acc.UserID != userID {
|
||||||
|
newAccounts = append(newAccounts, acc)
|
||||||
|
} else {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return errors.New("account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save remaining accounts back to file
|
||||||
|
newData, err := json.MarshalIndent(newAccounts, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(accountsFile, newData, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated timetable files
|
||||||
|
os.Remove(getTimetableFile(userID))
|
||||||
|
os.Remove(getTimetableFilledFile(userID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
// Ignore bot's own messages
|
// Ignore bot's own messages
|
||||||
if m.Author.ID == s.State.User.ID {
|
if m.Author.ID == s.State.User.ID {
|
||||||
|
|
@ -279,6 +379,29 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle "!deleteaccount" and "!removeaccount" commands
|
||||||
|
if m.GuildID != "" && (m.Content == "!deleteaccount" || m.Content == "!removeaccount") {
|
||||||
|
// Delete the command message for privacy
|
||||||
|
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
||||||
|
|
||||||
|
err := deleteAccount(m.Author.ID)
|
||||||
|
if err != nil {
|
||||||
|
// Create DM channel to notify of error
|
||||||
|
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||||
|
if err == nil {
|
||||||
|
s.ChannelMessageSend(channel.ID, "Failed to delete your account: "+err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify success via DM
|
||||||
|
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||||
|
if err == nil {
|
||||||
|
s.ChannelMessageSend(channel.ID, "Your account has been successfully deleted.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Handle DMs for the two-step process
|
// Handle DMs for the two-step process
|
||||||
if m.GuildID == "" {
|
if m.GuildID == "" {
|
||||||
stateMutex.Lock()
|
stateMutex.Lock()
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -11,8 +11,13 @@
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
### This bot is self hosted and will run correctly when doing "go run . " in the root folder of the project. Before usage add the .env file with the Credentials as mentioned above. When you run the Programm for the first time, all important files will be created automatically and the bot is ready to go. The user added via the fields UNTIS_USER and UNTIS_PASSWORD in the .env will be the one where the Webhook is sourced from and the other users will be send a DM after adding their account with the command.
|
# This is a discord application intended for self hosting. You will need to add an application in the disord developer website for this bot mainly with the read messages and write messages privelege as well as send privat messages for the account handling. If you give it administrator permissions you dont have to worry further but this could present you with a security risk if my github account gets compromised and a malicious update to the script gets pushed which you than use since the bot got access to your whole server.
|
||||||
|
|
||||||
## In case your school is using a different timing for the Lessons than mine, you can change the times where you will be notified with the next room and Lesson for the day in **Line 71 in the main.go** file.
|
|
||||||
|
|
||||||
# I am neither a representative of Untis Untis Baden-Württemberg GmbH nor a Developer in their team. This project is based on their API and is not affiliated with them.
|
### This bot is self hosted and will run correctly when doing "go run . " in the root folder of the project. **Before usage add the .env file with the Credentials as mentioned above.** When you run the Program for the first time, all important files will be created automatically and the bot is ready to go. The user added via the fields UNTIS_USER and UNTIS_PASSWORD in the .env will be the one where the Webhook is sourced from and the other users will be send a DM after adding their account with the command. Add the Discord Webhook via the DISCORD_WEBHOOK_URL point in the .env and will send the messages for the timetable in there.
|
||||||
|
|
||||||
|
### For multi user support: using the command **!addaccount** in the discord server the bot is used in, it will than dm you with the sign up process prompting you to enter your untis Username and password (no email neededd). The Username is stored as plain text and the password is hashed on the device hosting the bot, so currently it is **not secure** since the host will have access to the hashed password and the hash used! Deleting an account is done by using **!removeaccount** which will delete the account, created with you discord account.
|
||||||
|
|
||||||
|
## In case your school is using a different timing for the Lessons than mine, you can change the times where you will be notified with the next room and Lesson for the day in **Line 71 in the main.go** file. The bot may not send you a message if there is no lesson after the specified time so that you can leave empty lessons as is for the days.
|
||||||
|
|
||||||
|
# I am neither a representative of Untis, Untis Baden-Württemberg GmbH nor a Developer in their team. This project is based on their API and my code and is not affiliated with them.
|
||||||
156
main.go
156
main.go
|
|
@ -80,45 +80,74 @@ func isScheduledTime(now time.Time) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func scheduleTimetableUpdate() {
|
func scheduleTimetableUpdate() {
|
||||||
var prevData []byte
|
prevData := make(map[string][]byte)
|
||||||
//declare user and pass
|
|
||||||
godotenv.Load(".env")
|
// Function to update all users
|
||||||
var password = os.Getenv("UNTIS_PASSWORD")
|
updateAllUsers := func() {
|
||||||
var user = os.Getenv("UNTIS_USER")
|
// Load all registered users
|
||||||
Untis.Main(user, password)
|
accounts, err := BotStart.LoadAndDecryptAccounts()
|
||||||
// Initial read of the file
|
if err != nil {
|
||||||
data, err := os.ReadFile("timetableFilled.json")
|
log.Printf("Error loading accounts: %v", err)
|
||||||
if err == nil {
|
|
||||||
prevData = data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ticker for checking timetable changes every hour
|
// Add the default user from .env
|
||||||
hourTicker := time.NewTicker(1 * time.Minute)
|
defaultUser := os.Getenv("UNTIS_USER")
|
||||||
|
defaultPassword := os.Getenv("UNTIS_PASSWORD")
|
||||||
|
if defaultUser != "" && defaultPassword != "" {
|
||||||
|
accounts = append(accounts, BotStart.DecryptedAccount{
|
||||||
|
UserID: "default",
|
||||||
|
Username: defaultUser,
|
||||||
|
DecryptedPassword: defaultPassword,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each user
|
||||||
|
for _, acc := range accounts {
|
||||||
|
// Fetch the latest timetable
|
||||||
|
Untis.Main(acc.Username, acc.DecryptedPassword, acc.UserID)
|
||||||
|
|
||||||
|
// Check for changes
|
||||||
|
timetableFile := fmt.Sprintf("timetableFilled_%s.json", acc.UserID)
|
||||||
|
if acc.UserID == "default" {
|
||||||
|
timetableFile = "timetableFilled.json"
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(timetableFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading timetable for user %s: %v", acc.Username, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if prev, ok := prevData[acc.UserID]; ok && !bytes.Equal(data, prev) {
|
||||||
|
log.Printf("Timetable has changed for user %s.", acc.Username)
|
||||||
|
BotStart.SendDM(acc.UserID, "Your timetable has changed!")
|
||||||
|
}
|
||||||
|
prevData[acc.UserID] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateAllUsers()
|
||||||
|
|
||||||
|
// Ticker for periodic updates
|
||||||
|
hourTicker := time.NewTicker(1 * time.Minute) // For testing, 1 minute. Change to 1 * time.Hour for production.
|
||||||
go func() {
|
go func() {
|
||||||
for range hourTicker.C {
|
for range hourTicker.C {
|
||||||
Untis.Main(user, password)
|
updateAllUsers()
|
||||||
data, err := os.ReadFile("timetableFilled.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error reading timetable: %v", err)
|
|
||||||
} else if !bytes.Equal(data, prevData) {
|
|
||||||
log.Println("Timetable file has changed.")
|
|
||||||
sendUpdateDiscordWebhook()
|
|
||||||
prevData = data
|
|
||||||
}
|
|
||||||
// Trigger bot notifications for all users
|
|
||||||
BotStart.NotifyAllUsers()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Ticker for checking scheduled times every minute
|
// Ticker for scheduled lesson notifications
|
||||||
startMinuteTicker(func() {
|
startMinuteTicker(func() {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if isScheduledTime(now) {
|
if isScheduledTime(now) {
|
||||||
log.Println("Scheduled time reached, updating and running Run()")
|
log.Println("Scheduled time reached, running notifications...")
|
||||||
Untis.Main(user, password)
|
accounts, _ := BotStart.LoadAndDecryptAccounts()
|
||||||
log.Println("Updated now running Run()")
|
// Also notify the default user
|
||||||
Run()
|
accounts = append(accounts, BotStart.DecryptedAccount{UserID: "default"})
|
||||||
log.Println("Finished running Run")
|
for _, acc := range accounts {
|
||||||
|
Run(acc.UserID)
|
||||||
|
}
|
||||||
|
log.Println("Finished running notifications.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -136,42 +165,49 @@ func startMinuteTicker(f func()) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func Run(userID string) {
|
||||||
#Main Run Function
|
timetableFile := "timetableFilled.json"
|
||||||
*/
|
if userID != "default" {
|
||||||
func Run() {
|
timetableFile = fmt.Sprintf("timetableFilled_%s.json", userID)
|
||||||
log.Println("Sending next Lesson")
|
}
|
||||||
codeByStartTime, err := MapTimeToCode("timetableFilled.json")
|
log.Printf("Sending next lesson for user %s", userID)
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
// Check if the timetable file exists
|
||||||
|
if _, err := os.Stat(timetableFile); os.IsNotExist(err) {
|
||||||
|
log.Printf("Timetable file %s not found for user %s.", timetableFile, userID)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
roomByStartTime, err := MapTimeToRoom("timetableFilled.json")
|
codeByStartTime, _ := MapTimeToCode(timetableFile)
|
||||||
if err != nil {
|
roomByStartTime, _ := MapTimeToRoom(timetableFile)
|
||||||
log.Fatal(err)
|
subjectByStartTime, _ := MapTimeToSubject(timetableFile)
|
||||||
}
|
|
||||||
subjectByStartTime, err := MapTimeToSubject("timetableFilled.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
now := time.Now().Format("15:04")
|
now := time.Now().Format("15:04")
|
||||||
nextTime, room, found := NextRoomForTime(roomByStartTime, now)
|
nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, now)
|
||||||
if found {
|
if !foundRoom {
|
||||||
log.Println("found Room")
|
return // No upcoming lessons
|
||||||
Subject, found := NextSubjectForTime(subjectByStartTime, now)
|
|
||||||
if found {
|
|
||||||
log.Println("Found Subject")
|
|
||||||
Status, found := NextCodeForTime(codeByStartTime, now)
|
|
||||||
if found {
|
|
||||||
log.Println("Found Status")
|
|
||||||
fmt.Printf("Next time: %s, Room: %s\n", nextTime, room)
|
|
||||||
sendDiscordWebhook(Subject, room, nextTime, Status)
|
|
||||||
} else {
|
|
||||||
sendDiscordWebhook(Subject, room, nextTime, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subject, foundSubject := NextSubjectForTime(subjectByStartTime, now)
|
||||||
|
if !foundSubject {
|
||||||
|
return // Should not happen if room was found
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ := NextCodeForTime(codeByStartTime, now)
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if status != "" {
|
||||||
|
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**. Status: **%s**", subject, room, nextTime, status)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**.", subject, room, nextTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID == "default" {
|
||||||
|
// The default user still sends to the webhook
|
||||||
|
sendDiscordWebhook(subject, room, nextTime, status)
|
||||||
|
} else {
|
||||||
|
BotStart.SendDM(userID, message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue