diff --git a/Bot/Timetable.go b/Bot/Timetable.go index 74afefc..898cca4 100644 --- a/Bot/Timetable.go +++ b/Bot/Timetable.go @@ -88,67 +88,73 @@ func ReadLoginResultFromFile(path string) (Loginresult, error) { return result, err } -func Timetable(cookies []*http.Cookie) { - loginResult, err := ReadLoginResultFromFile("login.json") - if err != nil { - log.Fatal(err) +func Timetable(cookies []*http.Cookie, userID string) { + loginFile := "login.json" + if userID != "" { + 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") 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 { - log.Fatalf("Error marshaling login data: %v", err) + log.Printf("Error marshaling timetable request: %v", err) 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 { - log.Fatalf("Error creatingrequest: %v", err) + log.Printf("Error creating timetable request: %v", err) return } - //log.Println("prompt without extra header or cookie ", prompt) - //log.Println("Cookie: ", cookies) - - prompt.Header.Set("Content-Type", "application/json") - prompt.Header.Set("User-Agent", "Webuntis Test") - + req.Header.Set("Content-Type", "application/json") for _, cookie := range cookies { - //if cookie.Name == "JSESSIONID" { - prompt.AddCookie(cookie) - //log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value) - //} + req.AddCookie(cookie) } - //log.Println("Request JSON:", string(TeachersJson)) - out, err := http.DefaultClient.Do(prompt) + + resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error during request: %v", err) + log.Printf("Error fetching timetable: %v", err) return } - defer out.Body.Close() - //log.Println(out.Status) - response, err := io.ReadAll(out.Body) + defer resp.Body.Close() + + response, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading response body: %v", err) + log.Printf("Error reading timetable response: %v", err) return } - //responseString := string(response) - //log.Println("Repsonse ", responseString) + var Response TimetableResponse - err = json.Unmarshal(response, &Response) - if err != nil { - log.Fatalf("Error unmarshaling response: %v", err) + if err := json.Unmarshal(response, &Response); err != nil { + log.Printf("Error unmarshaling timetable response: %v", err) + return } + data, err := json.MarshalIndent(Response.Result, "", " ") 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 { - log.Fatal(err) + + timetableFile := "timetable.json" + if userID != "" { + timetableFile = "timetable_" + userID + ".json" } - log.Println("Updated Timetable") - setTimetable() + if err := os.WriteFile(timetableFile, data, 0644); err != nil { + 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) { @@ -189,14 +195,18 @@ func formatDate(date int) string { day := s[6:8] return fmt.Sprintf("%s-%s-%s", day, month, year) } -func setTimetable() { +func setTimetable(userID string) { subjects, _ := LoadIDMap("subjects.json") rooms, _ := LoadIDMap("rooms.json") classes, _ := LoadIDMap("classes.json") - timetable, _ := LoadTimetable("timetable.json") + + timetableFile := "timetable.json" + if userID != "" { + timetableFile = "timetable_" + userID + ".json" + } + timetable, _ := LoadTimetable(timetableFile) var namedTimetable []NamedTimetableEntry - for _, lesson := range timetable { var klNames, suNames, roNames []string for _, kl := range lesson.Kl { @@ -224,11 +234,17 @@ func setTimetable() { data, err := json.MarshalIndent(namedTimetable, "", " ") 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 { - log.Fatal(err) + + timetableFilledFile := "timetableFilled.json" + 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) } diff --git a/Bot/Untis.go b/Bot/Untis.go index 260afae..7b3faf9 100644 --- a/Bot/Untis.go +++ b/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 USERS = os.Getenv("UNTIS_USER") -func Main(user, password string) { +func Main(user, password, userID string) { godotenv.Load("../.env") - cookies, err := Auth(user, password) + cookies, err := Auth(user, password, userID) if err != nil { - log.Fatal(err) + log.Printf("Authentication failed for user %s: %v", user, err) return } Rooms(cookies) - Classes(cookies) - Subjects(cookies) - - Timetable(cookies) - - //getTeachers sends empty response + Timetable(cookies, userID) 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"} loginJSON, err := json.Marshal(l) if err != nil { - log.Fatalf("Error marshaling login data: %v", err) return nil, err } login := bytes.NewReader(loginJSON) - //log.Println("Run") LoginOut, err := http.Post(Url, "application/json", login) if err != nil { - log.Printf("Error during authentication: %v", err) return nil, err } defer LoginOut.Body.Close() - // Parse cookies from response cookies := LoginOut.Cookies() - //log.Println("Received cookies:", cookies) - //log.Println("Set-Cookie headers:", LoginOut.Header["Set-Cookie"]) - //loginRespBody, _ := io.ReadAll(LoginOut.Body) - //log.Println("Login response body:", string(loginRespBody)) + log.Printf("Login successful for user: %s", user) - log.Println("Login successful") - - //log.Println("Login successful") response, err := io.ReadAll(LoginOut.Body) if err != nil { - log.Fatalf("Error reading response body: %v", err) + return nil, err } + var Response LoginResponse - err = json.Unmarshal(response, &Response) - if err != nil { - log.Fatalf("Error unmarshaling response: %v", err) + if err := json.Unmarshal(response, &Response); err != nil { + return nil, err } + data, err := json.MarshalIndent(Response.Result, "", " ") if err != nil { - log.Fatal(err) + return nil, err } - err = os.WriteFile("login.json", data, 0644) - if err != nil { - log.Fatal(err) + + loginFile := "login.json" + if userID != "" { + loginFile = "login_" + userID + ".json" + } + if err := os.WriteFile(loginFile, data, 0644); err != nil { + return nil, err } return cookies, nil - } diff --git a/Botrun/BotStart.go b/Botrun/BotStart.go index 63a5c6d..a580dd3 100644 --- a/Botrun/BotStart.go +++ b/Botrun/BotStart.go @@ -248,6 +248,35 @@ func Start() { 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 func NotifyAllUsers() { if DiscordSession != nil { @@ -255,6 +284,27 @@ 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) + } +} + func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { // Ignore bot's own messages if m.Author.ID == s.State.User.ID { diff --git a/main.go b/main.go index 3751813..921ea3f 100644 --- a/main.go +++ b/main.go @@ -80,45 +80,74 @@ func isScheduledTime(now time.Time) bool { return false } func scheduleTimetableUpdate() { - var prevData []byte - //declare user and pass - godotenv.Load(".env") - var password = os.Getenv("UNTIS_PASSWORD") - var user = os.Getenv("UNTIS_USER") - Untis.Main(user, password) - // Initial read of the file - data, err := os.ReadFile("timetableFilled.json") - if err == nil { - prevData = data + prevData := make(map[string][]byte) + + // Function to update all users + updateAllUsers := func() { + // Load all registered users + accounts, err := BotStart.LoadAndDecryptAccounts() + if err != nil { + log.Printf("Error loading accounts: %v", err) + } + + // Add the default user from .env + 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 + } } - // Ticker for checking timetable changes every hour - hourTicker := time.NewTicker(1 * time.Minute) + // 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() { for range hourTicker.C { - Untis.Main(user, password) - 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() + updateAllUsers() } }() - // Ticker for checking scheduled times every minute + // Ticker for scheduled lesson notifications startMinuteTicker(func() { now := time.Now() if isScheduledTime(now) { - log.Println("Scheduled time reached, updating and running Run()") - Untis.Main(user, password) - log.Println("Updated now running Run()") - Run() - log.Println("Finished running Run") + log.Println("Scheduled time reached, running notifications...") + accounts, _ := BotStart.LoadAndDecryptAccounts() + // Also notify the default user + accounts = append(accounts, BotStart.DecryptedAccount{UserID: "default"}) + for _, acc := range accounts { + Run(acc.UserID) + } + log.Println("Finished running notifications.") } }) } @@ -136,42 +165,49 @@ func startMinuteTicker(f func()) { }() } -/* -#Main Run Function -*/ -func Run() { - log.Println("Sending next Lesson") - codeByStartTime, err := MapTimeToCode("timetableFilled.json") - if err != nil { - log.Println(err) +func Run(userID string) { + timetableFile := "timetableFilled.json" + if userID != "default" { + timetableFile = fmt.Sprintf("timetableFilled_%s.json", userID) + } + log.Printf("Sending next lesson for user %s", userID) + + // 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") - if err != nil { - log.Fatal(err) - } - subjectByStartTime, err := MapTimeToSubject("timetableFilled.json") - if err != nil { - log.Fatal(err) - } + codeByStartTime, _ := MapTimeToCode(timetableFile) + roomByStartTime, _ := MapTimeToRoom(timetableFile) + subjectByStartTime, _ := MapTimeToSubject(timetableFile) + now := time.Now().Format("15:04") - nextTime, room, found := NextRoomForTime(roomByStartTime, now) - if found { - log.Println("found Room") - 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, "") - } - } + nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, now) + if !foundRoom { + return // No upcoming lessons } + 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) + } } /*