diff --git a/.gitignore b/.gitignore index d830b8b..7bdcbe1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ rooms.json classes.json subjects.json teachers.json +login.json +timetable.json +timetableFilled.json \ No newline at end of file diff --git a/Bot/Classes.go b/Bot/Classes.go index c3d389c..8a356f5 100644 --- a/Bot/Classes.go +++ b/Bot/Classes.go @@ -9,7 +9,7 @@ import ( "os" ) -type classesResponse struct { +type ClassesResponse struct { Jsonrpc string `json:"jsonrpc"` ID string `json:"id"` Result []classes `json:"result"` @@ -69,7 +69,7 @@ func Classes(cookies []*http.Cookie) { } //responseString := string(response) //log.Println("Repsonse ", responseString) - var Response classesResponse + var Response ClassesResponse err = json.Unmarshal(response, &Response) if err != nil { log.Fatalf("Error unmarshaling response: %v", err) diff --git a/Bot/Subjects.go b/Bot/Subjects.go index a94543e..919e7a7 100644 --- a/Bot/Subjects.go +++ b/Bot/Subjects.go @@ -9,7 +9,7 @@ import ( "os" ) -type subjectsResponse struct { +type SubjectsResponse struct { Jsonrpc string `json:"jsonrpc"` ID string `json:"id"` Result []subjects `json:"result"` @@ -69,7 +69,7 @@ func Subjects(cookies []*http.Cookie) { } //responseString := string(response) //log.Println("Repsonse ", responseString) - var Response subjectsResponse + var Response SubjectsResponse err = json.Unmarshal(response, &Response) if err != nil { log.Fatalf("Error unmarshaling response: %v", err) diff --git a/Bot/Teachers.go b/Bot/Teachers.go index 95c5130..bdc6a91 100644 --- a/Bot/Teachers.go +++ b/Bot/Teachers.go @@ -9,7 +9,7 @@ import ( "os" ) -type teachersResponse struct { +type TeachersResponse struct { Jsonrpc string `json:"jsonrpc"` ID string `json:"id"` Result []teachers `json:"result"` @@ -69,7 +69,7 @@ func Teachers(cookies []*http.Cookie) { } //responseString := string(response) //log.Println("Repsonse ", responseString) - var Response teachersResponse + var Response TeachersResponse err = json.Unmarshal(response, &Response) if err != nil { log.Fatalf("Error unmarshaling response: %v", err) diff --git a/Bot/Timetable.go b/Bot/Timetable.go new file mode 100644 index 0000000..6b1a2ca --- /dev/null +++ b/Bot/Timetable.go @@ -0,0 +1,228 @@ +package Untis + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "time" +) + +type TimetableResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID string `json:"id"` + Result []timetable `json:"result"` +} +type NamedTimetableEntry struct { + ID int `json:"id"` + Date string `json:"date"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Code string `json:"code,omitempty"` + Statflags string `json:"statflags,omitempty"` + Kl []string `json:"kl"` + Su []string `json:"su"` + Ro []string `json:"ro"` + ActivityType string `json:"activityType"` +} +type timetable struct { + ID int `json:"id"` + Date int `json:"date"` + StartTime int `json:"startTime"` + EndTime int `json:"endTime"` + Code string `json:"code,omitempty"` + Statflags string `json:"statflags,omitempty"` + Kl []IDObj `json:"kl"` + Su []IDObj `json:"su"` + Ro []IDObj `json:"ro"` + ActivityType string `json:"activityType"` +} +type IDObj struct { + ID int `json:"id"` +} +type TimetableEntry struct { + ID int `json:"id"` + Date int `json:"date"` + StartTime int `json:"startTime"` + EndTime int `json:"endTime"` + Code string `json:"code,omitempty"` + Statflags string `json:"statflags,omitempty"` + Kl []IDObj `json:"kl"` + Su []IDObj `json:"su"` + Ro []IDObj `json:"ro"` + ActivityType string `json:"activityType"` +} +type NamedObj struct { + ID int `json:"id"` + Name string `json:"name"` +} +type getTimetable struct { + Id string `json:"id"` + Method string `json:"method"` + Params params `json:"params"` + Jsonrpc string `json:"jsonrpc"` +} +type params struct { + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + Id int `json:"id"` + Type int `json:"type"` +} + +func ReadLoginResultFromFile(path string) (Loginresult, error) { + var result Loginresult + data, err := os.ReadFile(path) + if err != nil { + return result, err + } + err = json.Unmarshal(data, &result) + return result, err +} + +func Timetable(cookies []*http.Cookie) { + loginResult, err := ReadLoginResultFromFile("login.json") + if err != nil { + log.Fatal(err) + } + 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) + if err != nil { + log.Fatalf("Error marshaling login data: %v", err) + return + } + timetable := bytes.NewReader(TimetablesJson) + + prompt, err := http.NewRequest("GET", Url, timetable) + if err != nil { + log.Fatalf("Error creatingrequest: %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") + + for _, cookie := range cookies { + //if cookie.Name == "JSESSIONID" { + 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) + if err != nil { + log.Printf("Error during request: %v", err) + return + } + defer out.Body.Close() + //log.Println(out.Status) + response, err := io.ReadAll(out.Body) + if err != nil { + log.Printf("Error reading response body: %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) + } + data, err := json.MarshalIndent(Response.Result, "", " ") + if err != nil { + log.Fatal(err) + } + err = os.WriteFile("timetable.json", data, 0644) + if err != nil { + log.Fatal(err) + } + log.Println("Updated Timetable") + setTimetable() +} + +func LoadIDMap(path string) (map[int]string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var objs []NamedObj + if err := json.Unmarshal(data, &objs); err != nil { + return nil, err + } + m := make(map[int]string) + for _, obj := range objs { + m[obj.ID] = obj.Name + } + return m, nil +} +func LoadTimetable(path string) ([]TimetableEntry, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var entries []TimetableEntry + if err := json.Unmarshal(data, &entries); err != nil { + return nil, err + } + return entries, nil +} +func formatTime(t int) string { + h := t / 100 + m := t % 100 + return fmt.Sprintf("%02d:%02d", h, m) +} +func formatDate(date int) string { + s := fmt.Sprintf("%08d", date) // ensures leading zeros + year := s[:4] + month := s[4:6] + day := s[6:8] + return fmt.Sprintf("%s-%s-%s", day, month, year) +} +func setTimetable() { + subjects, _ := LoadIDMap("subjects.json") + rooms, _ := LoadIDMap("rooms.json") + classes, _ := LoadIDMap("classes.json") + timetable, _ := LoadTimetable("timetable.json") + + var namedTimetable []NamedTimetableEntry + + for _, lesson := range timetable { + var klNames, suNames, roNames []string + for _, kl := range lesson.Kl { + klNames = append(klNames, classes[kl.ID]) + } + for _, su := range lesson.Su { + suNames = append(suNames, subjects[su.ID]) + } + for _, ro := range lesson.Ro { + roNames = append(roNames, rooms[ro.ID]) + } + namedTimetable = append(namedTimetable, NamedTimetableEntry{ + ID: lesson.ID, + Date: formatDate(lesson.Date), + StartTime: formatTime(lesson.StartTime), + EndTime: formatTime(lesson.EndTime), + Code: lesson.Code, + Statflags: lesson.Statflags, + Kl: klNames, + Su: suNames, + Ro: roNames, + ActivityType: lesson.ActivityType, + }) + } + + data, err := json.MarshalIndent(namedTimetable, "", " ") + if err != nil { + log.Fatal(err) + } + err = os.WriteFile("timetableFilled.json", data, 0644) + if err != nil { + log.Fatal(err) + } + log.Println("Filled Timetable") +} diff --git a/Bot/Untis.go b/Bot/Untis.go index 4722eb4..db0771b 100644 --- a/Bot/Untis.go +++ b/Bot/Untis.go @@ -3,6 +3,7 @@ package Untis import ( "bytes" "encoding/json" + "io" "log" "net/http" "os" @@ -23,6 +24,17 @@ type Login struct { Jsonrpc string `json:"jsonrpc"` } +type LoginResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID string `json:"id"` + Result Loginresult `json:"result"` +} +type Loginresult struct { + SessionID string `json:"sessionId"` + PersonType int `json:"personType"` + PersonID int `json:"personId"` + KlasseID int `json:"klasseId"` +} var Url = "https://thalia.webuntis.com/WebUntis/jsonrpc.do?school=Mons_Tabor" var Password = os.Getenv("UNTIS_PASSWORD") @@ -43,6 +55,7 @@ func Main() { //getTeachers sends empty response -> commented out //Teachers(cookies) + Timetable(cookies) } func Auth() ([]*http.Cookie, error) { @@ -68,7 +81,25 @@ func Auth() ([]*http.Cookie, error) { //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) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + var Response LoginResponse + err = json.Unmarshal(response, &Response) + if err != nil { + log.Fatalf("Error unmarshaling response: %v", err) + } + data, err := json.MarshalIndent(Response.Result, "", " ") + if err != nil { + log.Fatal(err) + } + err = os.WriteFile("login.json", data, 0644) + if err != nil { + log.Fatal(err) + } + return cookies, nil } diff --git a/main.go b/main.go index 6f23b52..8b82494 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,8 @@ import ( "log" "net/http" "os" - "os/signal" + "strings" "syscall" "time" Untis "untislogger/Bot" @@ -17,12 +17,20 @@ import ( "github.com/joho/godotenv" ) -// Global variable to store the current public IP -var currentPubIP string - -// Discord webhook configuration -var discordWebhookURL string // Webhook URL from environment variable +type NamedTimetableEntry struct { + ID int `json:"id"` + Date string `json:"date"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Code string `json:"code,omitempty"` + Statflags string `json:"statflags,omitempty"` + Kl []string `json:"kl"` + Su []string `json:"su"` + Ro []string `json:"ro"` + ActivityType string `json:"activityType"` +} +// init and main// func init() { err := godotenv.Load() if err != nil { @@ -38,12 +46,14 @@ func init() { } else { log.Println("No Discord webhook provided, Discord notifications will be disabled") } - - // scheduleIPCheck() // Start the periodic IP check } func main() { go Untis.Main() //starting API calls function + //checking for time + log.Println("starting Timetable checks") + go checkTimetableChanges() + go notifyUpcomingLessons() // Create a channel to receive OS signals sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) @@ -55,52 +65,102 @@ func main() { log.Println("Shutting down...") } -func testPubIP() { - resp, err := http.Get("https://api.ipify.org") +func loadTimetableFilled(path string) ([]NamedTimetableEntry, error) { + data, err := os.ReadFile(path) if err != nil { - log.Printf("Error getting public IP: %v", err) - return + return nil, err } - defer resp.Body.Close() - - ip, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Printf("Error reading response: %v", err) - return - } - - log.Printf("Current public IP: %s", ip) - if string(ip) != currentPubIP { - log.Printf("IP changed sending notifications for new IP: %s", ip) - currentPubIP = string(ip) - notifyNewPubIP(currentPubIP) + var entries []NamedTimetableEntry + if err := json.Unmarshal(data, &entries); err != nil { + return nil, err } + return entries, nil } -func scheduleIPCheck() { - // Run once immediately - testPubIP() +func checkTimetableChanges() { + var lastTimetable []NamedTimetableEntry - // Then schedule to run every hour - ticker := time.NewTicker(1 * time.Hour) - go func() { - for range ticker.C { - testPubIP() + for { + current, err := loadTimetableFilled("timetableFilled.json") + if err != nil { + log.Println("Error loading timetable:", err) + time.Sleep(time.Hour) + continue } - }() -} -func notifyNewPubIP(ip string) { - log.Printf("Notifying about new IP: %s", ip) - - // Send Discord notification if webhook is configured - if discordWebhookURL != "" { - sendDiscordWebhook(ip) - } else { - log.Println("Discord webhook not configured, skipping Discord notification") + // Compare with last timetable (simple string comparison) + log.Println("Checking if something changed") + if !equalTimetables(lastTimetable, current) && len(lastTimetable) > 0 { + // Find and send changes + changes := diffTimetables(lastTimetable, current) + oldMap := make(map[int]NamedTimetableEntry) + for _, e := range lastTimetable { + oldMap[e.ID] = e + } + for _, newLesson := range changes { + oldLesson, found := oldMap[newLesson.ID] + if found { + sendTimetableChangeWebhook(oldLesson, newLesson) + } else { + // If it's a new lesson, you can send a simple notification or skip + sendTimetableChangeWebhook(NamedTimetableEntry{}, newLesson) + } + } + } + lastTimetable = current + time.Sleep(time.Hour) } } +func equalTimetables(a, b []NamedTimetableEntry) bool { + + aj, _ := json.Marshal(a) + bj, _ := json.Marshal(b) + return bytes.Equal(aj, bj) +} + +func diffTimetables(old, new []NamedTimetableEntry) []NamedTimetableEntry { + // Simple diff: return lessons in new that are not in old + var diff []NamedTimetableEntry + oldMap := make(map[int]NamedTimetableEntry) + for _, e := range old { + oldMap[e.ID] = e + } + for _, e := range new { + if _, found := oldMap[e.ID]; !found { + diff = append(diff, e) + } + } + return diff +} + +func notifyUpcomingLessons() { + log.Println("checking for new Lesson") + for { + entries, err := loadTimetableFilled("timetableFilled.json") + if err != nil { + log.Println("Error loading timetable:", err) + time.Sleep(time.Minute) + continue + } + now := time.Now() + for _, lesson := range entries { + // Parse date and time + lessonTime, err := time.Parse("02-01-2006 15:04", lesson.Date+" "+lesson.StartTime) + if err != nil { + continue + } + if lessonTime.Sub(now) > 4*time.Minute && lessonTime.Sub(now) < 6*time.Minute { + sendNextLessonWebhook(lesson) + } + } + time.Sleep(time.Minute) + } +} + +// Discord webhook configuration +var discordWebhookURL string // Webhook URL from environment variable + // DiscordWebhookPayload represents the structure for Discord webhook messages type DiscordWebhookPayload struct { Content string `json:"content"` @@ -121,6 +181,54 @@ type Field struct { Inline bool `json:"inline"` } +func sendTimetableChangeWebhook(oldLesson, newLesson NamedTimetableEntry) { + var changes []string + if !equalStringSlices(oldLesson.Su, newLesson.Su) { + changes = append(changes, fmt.Sprintf("Subject: %v → %v", strings.Join(oldLesson.Su, ", "), strings.Join(newLesson.Su, ", "))) + } + if !equalStringSlices(oldLesson.Ro, newLesson.Ro) { + changes = append(changes, fmt.Sprintf("Room: %v → %v", strings.Join(oldLesson.Ro, ", "), strings.Join(newLesson.Ro, ", "))) + } + if oldLesson.StartTime != newLesson.StartTime { + changes = append(changes, fmt.Sprintf("Time: %s → %s", oldLesson.StartTime, newLesson.StartTime)) + } + if oldLesson.Code != newLesson.Code { + changes = append(changes, fmt.Sprintf("Code: %s → %s", oldLesson.Code, newLesson.Code)) + } + if len(changes) == 0 { + changes = append(changes, "Other details changed") + } + + content := fmt.Sprintf("**Timetable Change**\nLesson: %v\nTime: %s\nChanged: %s", + strings.Join(newLesson.Su, ", "), + newLesson.StartTime, + strings.Join(changes, "; "), + ) + + sendDiscordWebhook(content) +} + +func sendNextLessonWebhook(lesson NamedTimetableEntry) { + content := fmt.Sprintf( + "**Next Lesson Reminder**\nLesson: %v\nTime: %s\nRoom: %v", + strings.Join(lesson.Su, ", "), + lesson.StartTime, + strings.Join(lesson.Ro, ", "), + ) + sendDiscordWebhook(content) +} + +func equalStringSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} func sendDiscordWebhook(ip string) { log.Println("Sending Discord webhook notification...")