Working timetable

This commit is contained in:
Jannover5000r 2025-06-25 01:04:31 +02:00
parent 1bd4de6bf4
commit 58415419bc
7 changed files with 420 additions and 50 deletions

3
.gitignore vendored
View file

@ -3,3 +3,6 @@ rooms.json
classes.json classes.json
subjects.json subjects.json
teachers.json teachers.json
login.json
timetable.json
timetableFilled.json

View file

@ -9,7 +9,7 @@ import (
"os" "os"
) )
type classesResponse struct { type ClassesResponse struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID string `json:"id"` ID string `json:"id"`
Result []classes `json:"result"` Result []classes `json:"result"`
@ -69,7 +69,7 @@ func Classes(cookies []*http.Cookie) {
} }
//responseString := string(response) //responseString := string(response)
//log.Println("Repsonse ", responseString) //log.Println("Repsonse ", responseString)
var Response classesResponse var Response ClassesResponse
err = json.Unmarshal(response, &Response) err = json.Unmarshal(response, &Response)
if err != nil { if err != nil {
log.Fatalf("Error unmarshaling response: %v", err) log.Fatalf("Error unmarshaling response: %v", err)

View file

@ -9,7 +9,7 @@ import (
"os" "os"
) )
type subjectsResponse struct { type SubjectsResponse struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID string `json:"id"` ID string `json:"id"`
Result []subjects `json:"result"` Result []subjects `json:"result"`
@ -69,7 +69,7 @@ func Subjects(cookies []*http.Cookie) {
} }
//responseString := string(response) //responseString := string(response)
//log.Println("Repsonse ", responseString) //log.Println("Repsonse ", responseString)
var Response subjectsResponse var Response SubjectsResponse
err = json.Unmarshal(response, &Response) err = json.Unmarshal(response, &Response)
if err != nil { if err != nil {
log.Fatalf("Error unmarshaling response: %v", err) log.Fatalf("Error unmarshaling response: %v", err)

View file

@ -9,7 +9,7 @@ import (
"os" "os"
) )
type teachersResponse struct { type TeachersResponse struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID string `json:"id"` ID string `json:"id"`
Result []teachers `json:"result"` Result []teachers `json:"result"`
@ -69,7 +69,7 @@ func Teachers(cookies []*http.Cookie) {
} }
//responseString := string(response) //responseString := string(response)
//log.Println("Repsonse ", responseString) //log.Println("Repsonse ", responseString)
var Response teachersResponse var Response TeachersResponse
err = json.Unmarshal(response, &Response) err = json.Unmarshal(response, &Response)
if err != nil { if err != nil {
log.Fatalf("Error unmarshaling response: %v", err) log.Fatalf("Error unmarshaling response: %v", err)

228
Bot/Timetable.go Normal file
View file

@ -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")
}

View file

@ -3,6 +3,7 @@ package Untis
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -23,6 +24,17 @@ type Login struct {
Jsonrpc string `json:"jsonrpc"` 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 Url = "https://thalia.webuntis.com/WebUntis/jsonrpc.do?school=Mons_Tabor"
var Password = os.Getenv("UNTIS_PASSWORD") var Password = os.Getenv("UNTIS_PASSWORD")
@ -43,6 +55,7 @@ func Main() {
//getTeachers sends empty response -> commented out //getTeachers sends empty response -> commented out
//Teachers(cookies) //Teachers(cookies)
Timetable(cookies)
} }
func Auth() ([]*http.Cookie, error) { func Auth() ([]*http.Cookie, error) {
@ -68,7 +81,25 @@ func Auth() ([]*http.Cookie, error) {
//log.Println("Set-Cookie headers:", LoginOut.Header["Set-Cookie"]) //log.Println("Set-Cookie headers:", LoginOut.Header["Set-Cookie"])
//loginRespBody, _ := io.ReadAll(LoginOut.Body) //loginRespBody, _ := io.ReadAll(LoginOut.Body)
//log.Println("Login response body:", string(loginRespBody)) //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 return cookies, nil
} }

194
main.go
View file

@ -8,8 +8,8 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
Untis "untislogger/Bot" Untis "untislogger/Bot"
@ -17,12 +17,20 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
// Global variable to store the current public IP type NamedTimetableEntry struct {
var currentPubIP string ID int `json:"id"`
Date string `json:"date"`
// Discord webhook configuration StartTime string `json:"startTime"`
var discordWebhookURL string // Webhook URL from environment variable 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() { func init() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
@ -38,12 +46,14 @@ func init() {
} else { } else {
log.Println("No Discord webhook provided, Discord notifications will be disabled") log.Println("No Discord webhook provided, Discord notifications will be disabled")
} }
// scheduleIPCheck() // Start the periodic IP check
} }
func main() { func main() {
go Untis.Main() //starting API calls function 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 // Create a channel to receive OS signals
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
@ -55,52 +65,102 @@ func main() {
log.Println("Shutting down...") log.Println("Shutting down...")
} }
func testPubIP() { func loadTimetableFilled(path string) ([]NamedTimetableEntry, error) {
resp, err := http.Get("https://api.ipify.org") data, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Printf("Error getting public IP: %v", err) return nil, err
return
} }
defer resp.Body.Close() var entries []NamedTimetableEntry
if err := json.Unmarshal(data, &entries); err != nil {
ip, err := ioutil.ReadAll(resp.Body) return nil, err
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)
} }
return entries, nil
} }
func scheduleIPCheck() { func checkTimetableChanges() {
// Run once immediately var lastTimetable []NamedTimetableEntry
testPubIP()
// Then schedule to run every hour for {
ticker := time.NewTicker(1 * time.Hour) current, err := loadTimetableFilled("timetableFilled.json")
go func() { if err != nil {
for range ticker.C { log.Println("Error loading timetable:", err)
testPubIP() time.Sleep(time.Hour)
continue
} }
}()
}
func notifyNewPubIP(ip string) { // Compare with last timetable (simple string comparison)
log.Printf("Notifying about new IP: %s", ip) log.Println("Checking if something changed")
if !equalTimetables(lastTimetable, current) && len(lastTimetable) > 0 {
// Send Discord notification if webhook is configured // Find and send changes
if discordWebhookURL != "" { changes := diffTimetables(lastTimetable, current)
sendDiscordWebhook(ip) oldMap := make(map[int]NamedTimetableEntry)
} else { for _, e := range lastTimetable {
log.Println("Discord webhook not configured, skipping Discord notification") 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 // DiscordWebhookPayload represents the structure for Discord webhook messages
type DiscordWebhookPayload struct { type DiscordWebhookPayload struct {
Content string `json:"content"` Content string `json:"content"`
@ -121,6 +181,54 @@ type Field struct {
Inline bool `json:"inline"` 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) { func sendDiscordWebhook(ip string) {
log.Println("Sending Discord webhook notification...") log.Println("Sending Discord webhook notification...")