Compare commits
No commits in common. "main" and "release" have entirely different histories.
17 changed files with 417 additions and 906 deletions
|
|
@ -1,7 +0,0 @@
|
||||||
DISCORD_WEBHOOK_URL=[Paste Webhook]
|
|
||||||
UNTIS_PASSWORD=[Default user passwd]
|
|
||||||
UNTIS_USER=[Default user username]
|
|
||||||
DISCORD_BOT_TOKEN=[Paste Bot token]
|
|
||||||
ENC_KEY=[Generate enc key and paste]
|
|
||||||
URL=[Untis url, create from tutorial in readme]
|
|
||||||
LOCATION_ENV=Europe/Berlin [change to your timezone, needs to be usable by go's time package via time.LoadLocation()]
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -6,10 +6,4 @@ teachers.json
|
||||||
login.json
|
login.json
|
||||||
timetable*.json
|
timetable*.json
|
||||||
timetableFilled*.json
|
timetableFilled*.json
|
||||||
accounts/
|
accounts.json
|
||||||
login_*.json
|
|
||||||
AGENTS.md
|
|
||||||
untislogger
|
|
||||||
untislogger*
|
|
||||||
CHANGES_SUMMARY.md
|
|
||||||
|
|
||||||
|
|
@ -32,57 +32,55 @@ func Classes(cookies []*http.Cookie) {
|
||||||
g := getClasses{"2023-05-06 15:44:22.215292", "getKlassen", map[string]interface{}{}, "2.0"}
|
g := getClasses{"2023-05-06 15:44:22.215292", "getKlassen", map[string]interface{}{}, "2.0"}
|
||||||
ClassesJson, err := json.Marshal(g)
|
ClassesJson, err := json.Marshal(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshaling login data: %v", err)
|
log.Fatalf("Error marshaling login data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
classes := bytes.NewReader(ClassesJson)
|
classes := bytes.NewReader(ClassesJson)
|
||||||
|
|
||||||
prompt, err := http.NewRequest("POST", Url, classes)
|
prompt, err := http.NewRequest("POST", Url, classes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creatingrequest: %v", err)
|
log.Fatalf("Error creatingrequest: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log.Println("prompt without extra header or cookie ", prompt)
|
//log.Println("prompt without extra header or cookie ", prompt)
|
||||||
// log.Println("Cookie: ", cookies)
|
//log.Println("Cookie: ", cookies)
|
||||||
|
|
||||||
prompt.Header.Set("Content-Type", "application/json")
|
prompt.Header.Set("Content-Type", "application/json")
|
||||||
prompt.Header.Set("User-Agent", "Webuntis Test")
|
prompt.Header.Set("User-Agent", "Webuntis Test")
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
// if cookie.Name == "JSESSIONID" {
|
//if cookie.Name == "JSESSIONID" {
|
||||||
prompt.AddCookie(cookie)
|
prompt.AddCookie(cookie)
|
||||||
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
// log.Println("Request JSON:", string(ClassesJson))
|
//log.Println("Request JSON:", string(ClassesJson))
|
||||||
out, err := http.DefaultClient.Do(prompt)
|
out, err := http.DefaultClient.Do(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during request: %v", err)
|
log.Printf("Error during request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Body.Close()
|
defer out.Body.Close()
|
||||||
// log.Println(out.Status)
|
//log.Println(out.Status)
|
||||||
response, err := io.ReadAll(out.Body)
|
response, err := io.ReadAll(out.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading response body: %v", err)
|
log.Printf("Error reading response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 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.Printf("Error unmarshaling response: %v", 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.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err = os.WriteFile("classes.json", data, 0o644)
|
err = os.WriteFile("classes.json", data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// log.Println("Updated Classes")//dont need log anymore
|
log.Println("Updated Classes")
|
||||||
}
|
}
|
||||||
|
|
@ -31,63 +31,60 @@ type Room struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Rooms(cookies []*http.Cookie) {
|
func Rooms(cookies []*http.Cookie) {
|
||||||
// log.Println("Abrufen der Stunden")
|
//log.Println("Abrufen der Stunden")
|
||||||
g := getRooms{"2023-05-06 15:44:22.215292", "getRooms", map[string]interface{}{}, "2.0"}
|
g := getRooms{"2023-05-06 15:44:22.215292", "getRooms", map[string]interface{}{}, "2.0"}
|
||||||
roomsJson, err := json.Marshal(g)
|
roomsJson, err := json.Marshal(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshaling login data: %v", err)
|
log.Fatalf("Error marshaling login data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rooms := bytes.NewReader(roomsJson)
|
rooms := bytes.NewReader(roomsJson)
|
||||||
|
|
||||||
prompt, err := http.NewRequest("POST", Url, rooms)
|
prompt, err := http.NewRequest("POST", Url, rooms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creatingrequest: %v", err)
|
log.Fatalf("Error creatingrequest: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log.Println("prompt without extra header or cookie ", prompt)
|
//log.Println("prompt without extra header or cookie ", prompt)
|
||||||
// log.Println("Cookie: ", cookies)
|
//log.Println("Cookie: ", cookies)
|
||||||
|
|
||||||
prompt.Header.Set("Content-Type", "application/json")
|
prompt.Header.Set("Content-Type", "application/json")
|
||||||
prompt.Header.Set("User-Agent", "Webuntis Test")
|
prompt.Header.Set("User-Agent", "Webuntis Test")
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
// if cookie.Name == "JSESSIONID" {
|
//if cookie.Name == "JSESSIONID" {
|
||||||
prompt.AddCookie(cookie)
|
prompt.AddCookie(cookie)
|
||||||
// log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
// log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
// log.Println("Request JSON:", string(roomsJson))
|
//log.Println("Request JSON:", string(roomsJson))
|
||||||
out, err := http.DefaultClient.Do(prompt)
|
out, err := http.DefaultClient.Do(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during request: %v", err)
|
log.Printf("Error during request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Body.Close()
|
defer out.Body.Close()
|
||||||
// log.Println(out.Status)
|
//log.Println(out.Status)
|
||||||
response, err := io.ReadAll(out.Body)
|
response, err := io.ReadAll(out.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading response body: %v", err)
|
log.Printf("Error reading response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// responseString := string(response)
|
//responseString := string(response)
|
||||||
// log.Println("Repsonse ", responseString)
|
//log.Println("Repsonse ", responseString)
|
||||||
var Response RoomsResponse
|
var Response RoomsResponse
|
||||||
err = json.Unmarshal(response, &Response)
|
err = json.Unmarshal(response, &Response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error unmarshaling 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.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err = os.WriteFile("rooms.json", data, 0o644)
|
err = os.WriteFile("rooms.json", data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// log.Println("Updated Rooms")//dont need log anymore
|
log.Println("Updated Rooms")
|
||||||
}
|
}
|
||||||
|
|
@ -32,58 +32,55 @@ func Subjects(cookies []*http.Cookie) {
|
||||||
g := getSubjects{"2023-05-06 15:44:22.215292", "getSubjects", map[string]interface{}{}, "2.0"}
|
g := getSubjects{"2023-05-06 15:44:22.215292", "getSubjects", map[string]interface{}{}, "2.0"}
|
||||||
SubjectsJson, err := json.Marshal(g)
|
SubjectsJson, err := json.Marshal(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshaling login data: %v", err)
|
log.Fatalf("Error marshaling login data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
subjects := bytes.NewReader(SubjectsJson)
|
subjects := bytes.NewReader(SubjectsJson)
|
||||||
|
|
||||||
prompt, err := http.NewRequest("POST", Url, subjects)
|
prompt, err := http.NewRequest("POST", Url, subjects)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creatingrequest: %v", err)
|
log.Fatalf("Error creatingrequest: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log.Println("prompt without extra header or cookie ", prompt)
|
//log.Println("prompt without extra header or cookie ", prompt)
|
||||||
// log.Println("Cookie: ", cookies)
|
//log.Println("Cookie: ", cookies)
|
||||||
|
|
||||||
prompt.Header.Set("Content-Type", "application/json")
|
prompt.Header.Set("Content-Type", "application/json")
|
||||||
prompt.Header.Set("User-Agent", "Webuntis Test")
|
prompt.Header.Set("User-Agent", "Webuntis Test")
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
// if cookie.Name == "JSESSIONID" {
|
//if cookie.Name == "JSESSIONID" {
|
||||||
prompt.AddCookie(cookie)
|
prompt.AddCookie(cookie)
|
||||||
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
// log.Println("Request JSON:", string(SubjectsJson))
|
//log.Println("Request JSON:", string(SubjectsJson))
|
||||||
out, err := http.DefaultClient.Do(prompt)
|
out, err := http.DefaultClient.Do(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during request: %v", err)
|
log.Printf("Error during request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Body.Close()
|
defer out.Body.Close()
|
||||||
// log.Println(out.Status)
|
//log.Println(out.Status)
|
||||||
response, err := io.ReadAll(out.Body)
|
response, err := io.ReadAll(out.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading response body: %v", err)
|
log.Printf("Error reading response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 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.Printf("Error unmarshaling 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.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err = os.WriteFile("subjects.json", data, 0o644)
|
err = os.WriteFile("subjects.json", data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// log.Println("Updated Subjects")//dont need log anymore
|
log.Println("Updated Subjects")
|
||||||
}
|
}
|
||||||
|
|
@ -32,58 +32,55 @@ func Teachers(cookies []*http.Cookie) {
|
||||||
g := getTeachers{"2023-05-06 15:44:22.215292", "getTeachers", map[string]interface{}{}, "2.0"}
|
g := getTeachers{"2023-05-06 15:44:22.215292", "getTeachers", map[string]interface{}{}, "2.0"}
|
||||||
TeachersJson, err := json.Marshal(g)
|
TeachersJson, err := json.Marshal(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshaling login data: %v", err)
|
log.Fatalf("Error marshaling login data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
teachers := bytes.NewReader(TeachersJson)
|
teachers := bytes.NewReader(TeachersJson)
|
||||||
|
|
||||||
prompt, err := http.NewRequest("POST", Url, teachers)
|
prompt, err := http.NewRequest("POST", Url, teachers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creatingrequest: %v", err)
|
log.Fatalf("Error creatingrequest: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log.Println("prompt without extra header or cookie ", prompt)
|
//log.Println("prompt without extra header or cookie ", prompt)
|
||||||
// log.Println("Cookie: ", cookies)
|
//log.Println("Cookie: ", cookies)
|
||||||
|
|
||||||
prompt.Header.Set("Content-Type", "application/json")
|
prompt.Header.Set("Content-Type", "application/json")
|
||||||
prompt.Header.Set("User-Agent", "Webuntis Test")
|
prompt.Header.Set("User-Agent", "Webuntis Test")
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
// if cookie.Name == "JSESSIONID" {
|
//if cookie.Name == "JSESSIONID" {
|
||||||
prompt.AddCookie(cookie)
|
prompt.AddCookie(cookie)
|
||||||
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
// log.Println("Request JSON:", string(TeachersJson))
|
//log.Println("Request JSON:", string(TeachersJson))
|
||||||
out, err := http.DefaultClient.Do(prompt)
|
out, err := http.DefaultClient.Do(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error during request: %v", err)
|
log.Printf("Error during request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Body.Close()
|
defer out.Body.Close()
|
||||||
// log.Println(out.Status)
|
//log.Println(out.Status)
|
||||||
response, err := io.ReadAll(out.Body)
|
response, err := io.ReadAll(out.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading response body: %v", err)
|
log.Printf("Error reading response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 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.Printf("Error unmarshaling 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.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err = os.WriteFile("teachers.json", data, 0o644)
|
err = os.WriteFile("teachers.json", data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// log.Println("Updated Teachers")//dont need log anymore
|
log.Println("Updated Teachers")
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TimetableResponse struct {
|
type TimetableResponse struct {
|
||||||
|
|
@ -87,73 +88,67 @@ func ReadLoginResultFromFile(path string) (Loginresult, error) {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Timetable(cookies []*http.Cookie, userID string) {
|
func Timetable(cookies []*http.Cookie) {
|
||||||
loginFile := "login.json"
|
loginResult, err := ReadLoginResultFromFile("login.json")
|
||||||
if userID != "" {
|
|
||||||
loginFile = "login_" + userID + ".json"
|
|
||||||
}
|
|
||||||
|
|
||||||
loginResult, err := ReadLoginResultFromFile(loginFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not read login result for user %s: %v", userID, err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
today := time.Now().Format("20060102")
|
||||||
today := GetTime().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.Printf("Error marshaling timetable request: %v", err)
|
log.Fatalf("Error marshaling login data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timetable := bytes.NewReader(timetablesJson)
|
timetable := bytes.NewReader(TimetablesJson)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", Url, timetable)
|
prompt, err := http.NewRequest("GET", Url, timetable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating timetable request: %v", err)
|
log.Fatalf("Error creatingrequest: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
//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 {
|
for _, cookie := range cookies {
|
||||||
req.AddCookie(cookie)
|
//if cookie.Name == "JSESSIONID" {
|
||||||
|
prompt.AddCookie(cookie)
|
||||||
|
//log.Printf("Added JSESSIONID cookie: %s=%s", cookie.Name, cookie.Value)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
//log.Println("Request JSON:", string(TeachersJson))
|
||||||
resp, err := http.DefaultClient.Do(req)
|
out, err := http.DefaultClient.Do(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error fetching timetable: %v", err)
|
log.Printf("Error during request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer out.Body.Close()
|
||||||
|
//log.Println(out.Status)
|
||||||
response, err := io.ReadAll(resp.Body)
|
response, err := io.ReadAll(out.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading timetable response: %v", err)
|
log.Printf("Error reading response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//responseString := string(response)
|
||||||
|
//log.Println("Repsonse ", responseString)
|
||||||
var Response TimetableResponse
|
var Response TimetableResponse
|
||||||
if err := json.Unmarshal(response, &Response); err != nil {
|
err = json.Unmarshal(response, &Response)
|
||||||
log.Printf("Error unmarshaling timetable response: %v", err)
|
if err != nil {
|
||||||
return
|
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.Printf("Error marshaling timetable result: %v", err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
err = os.WriteFile("timetable.json", data, 0644)
|
||||||
timetableFile := "timetable.json"
|
if err != nil {
|
||||||
if userID != "" {
|
log.Fatal(err)
|
||||||
timetableFile = "timetable_" + userID + ".json"
|
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(timetableFile, data, 0o644); err != nil {
|
log.Println("Updated Timetable")
|
||||||
log.Printf("Error writing timetable file: %v", err)
|
setTimetable()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// log.Printf("Updated timetable for user %s", userID) // dont need log anymore
|
|
||||||
setTimetable(userID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadIDMap(path string) (map[int]string, error) {
|
func LoadIDMap(path string) (map[int]string, error) {
|
||||||
|
|
@ -171,7 +166,6 @@ func LoadIDMap(path string) (map[int]string, error) {
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadTimetable(path string) ([]TimetableEntry, error) {
|
func LoadTimetable(path string) ([]TimetableEntry, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -183,13 +177,11 @@ func LoadTimetable(path string) ([]TimetableEntry, error) {
|
||||||
}
|
}
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTime(t int) string {
|
func formatTime(t int) string {
|
||||||
h := t / 100
|
h := t / 100
|
||||||
m := t % 100
|
m := t % 100
|
||||||
return fmt.Sprintf("%02d:%02d", h, m)
|
return fmt.Sprintf("%02d:%02d", h, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDate(date int) string {
|
func formatDate(date int) string {
|
||||||
s := fmt.Sprintf("%08d", date) // ensures leading zeros
|
s := fmt.Sprintf("%08d", date) // ensures leading zeros
|
||||||
year := s[:4]
|
year := s[:4]
|
||||||
|
|
@ -197,19 +189,14 @@ 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 {
|
||||||
|
|
@ -237,17 +224,11 @@ func setTimetable(userID string) {
|
||||||
|
|
||||||
data, err := json.MarshalIndent(namedTimetable, "", " ")
|
data, err := json.MarshalIndent(namedTimetable, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshaling named timetable: %v", err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
err = os.WriteFile("timetableFilled.json", data, 0644)
|
||||||
timetableFilledFile := "timetableFilled.json"
|
if err != nil {
|
||||||
if userID != "" {
|
log.Fatal(err)
|
||||||
timetableFilledFile = "timetableFilled_" + userID + ".json"
|
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(timetableFilledFile, data, 0o644); err != nil {
|
log.Println("Filled Timetable")
|
||||||
log.Printf("Error writing timetableFilled file: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// log.Printf("Filled timetable for user %s", userID)//log not needed
|
|
||||||
}
|
}
|
||||||
104
Bot/Untis.go
Normal file
104
Bot/Untis.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package Untis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
Users string `json:"user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Client string `json:"client"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Login struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params Params `json:"params"`
|
||||||
|
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
}
|
||||||
|
type LoginResponse struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Result Loginresult `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
godotenv.Load("../.env")
|
||||||
|
cookies, err := Auth(user, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Rooms(cookies)
|
||||||
|
|
||||||
|
Classes(cookies)
|
||||||
|
|
||||||
|
Subjects(cookies)
|
||||||
|
|
||||||
|
Timetable(cookies)
|
||||||
|
|
||||||
|
//getTeachers sends empty response
|
||||||
|
Teachers(cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Auth(user, password 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.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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,11 +12,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
"untislogger/Untis"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
|
|
@ -39,15 +37,13 @@ type UserState struct {
|
||||||
var (
|
var (
|
||||||
userStates = make(map[string]*UserState) // userID -> state
|
userStates = make(map[string]*UserState) // userID -> state
|
||||||
stateMutex sync.Mutex // protect userStates
|
stateMutex sync.Mutex // protect userStates
|
||||||
accountsFile = "accounts/accounts.json"
|
accountsFile = "accounts.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var encryptionKey []byte
|
var encryptionKey []byte
|
||||||
|
|
||||||
var WebHook = true
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load(".env")
|
godotenv.Load("/home/Jannik/Documents/Code/Go/Untis/Untis-Logger/.env")
|
||||||
keyStr := os.Getenv("ENC_KEY")
|
keyStr := os.Getenv("ENC_KEY")
|
||||||
if keyStr == "" {
|
if keyStr == "" {
|
||||||
panic("ENC_KEY environment variable not set")
|
panic("ENC_KEY environment variable not set")
|
||||||
|
|
@ -108,12 +104,6 @@ func decrypt(encrypted string) (string, error) {
|
||||||
func saveAccount(userID, username, password string) error {
|
func saveAccount(userID, username, password string) error {
|
||||||
var accounts []Account
|
var accounts []Account
|
||||||
|
|
||||||
// Ensure the accounts directory exists
|
|
||||||
accountsDir := filepath.Dir(accountsFile)
|
|
||||||
if err := os.MkdirAll(accountsDir, 0o755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create accounts directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing accounts if file exists
|
// Load existing accounts if file exists
|
||||||
if data, err := os.ReadFile(accountsFile); err == nil && len(data) > 0 {
|
if data, err := os.ReadFile(accountsFile); err == nil && len(data) > 0 {
|
||||||
_ = json.Unmarshal(data, &accounts)
|
_ = json.Unmarshal(data, &accounts)
|
||||||
|
|
@ -144,21 +134,7 @@ func saveAccount(userID, username, password string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(accountsFile, newData, 0o644); err != nil {
|
return os.WriteFile(accountsFile, newData, 0644)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure timetable files exist for this user
|
|
||||||
timetableFile := getTimetableFile(userID)
|
|
||||||
timetableFilledFile := getTimetableFilledFile(userID)
|
|
||||||
if _, err := os.Stat(timetableFile); os.IsNotExist(err) {
|
|
||||||
os.WriteFile(timetableFile, []byte("[]"), 0o644)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(timetableFilledFile); os.IsNotExist(err) {
|
|
||||||
os.WriteFile(timetableFilledFile, []byte("[]"), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for per-user timetable files
|
// Helper functions for per-user timetable files
|
||||||
|
|
@ -188,25 +164,14 @@ func checkTimetableChangesForUser(user Account, decPwd string, s *discordgo.Sess
|
||||||
// Load previous timetable
|
// Load previous timetable
|
||||||
prevData, _ := os.ReadFile(timetableFilledFile)
|
prevData, _ := os.ReadFile(timetableFilledFile)
|
||||||
|
|
||||||
// Fetch new timetable for this user
|
// TODO: Fetch new timetable for this user using user.Username and decrypted password
|
||||||
cookies, err := Untis.Auth(user.Username, decPwd, user.UserID)
|
// For demonstration, we'll just copy prevData (replace this with real fetch logic!)
|
||||||
if err != nil {
|
newData := prevData
|
||||||
fmt.Printf("Error authenticating user %s: %v\n", user.Username, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Untis.Timetable(cookies, user.UserID)
|
|
||||||
|
|
||||||
// Load the newly fetched timetable data
|
|
||||||
newData, err := os.ReadFile(timetableFilledFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading new timetable for user %s: %v\n", user.Username, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare and notify if changed
|
// Compare and notify if changed
|
||||||
if !bytes.Equal(newData, prevData) {
|
if !bytes.Equal(newData, prevData) {
|
||||||
// The main.go now handles detailed change notifications
|
sendLessonNotification(s, user.UserID, user.Username, "Your timetable has changed!")
|
||||||
// This function is kept for compatibility but no longer sends generic messages
|
os.WriteFile(timetableFilledFile, newData, 0644)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,12 +189,11 @@ func checkAllUsersTimetables(s *discordgo.Session) {
|
||||||
accounts := loadAllAccounts()
|
accounts := loadAllAccounts()
|
||||||
for _, user := range accounts {
|
for _, user := range accounts {
|
||||||
decPwd, _ := decrypt(user.Password)
|
decPwd, _ := decrypt(user.Password)
|
||||||
|
// You can use decPwd to fetch the timetable for this user
|
||||||
checkTimetableChangesForUser(user, decPwd, s)
|
checkTimetableChangesForUser(user, decPwd, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var DiscordSession *discordgo.Session
|
|
||||||
|
|
||||||
func Start() {
|
func Start() {
|
||||||
token := os.Getenv("DISCORD_BOT_TOKEN")
|
token := os.Getenv("DISCORD_BOT_TOKEN")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
|
|
@ -242,7 +206,6 @@ func Start() {
|
||||||
fmt.Println("error creating Discord session,", err)
|
fmt.Println("error creating Discord session,", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DiscordSession = dg // Save session for use elsewhere
|
|
||||||
|
|
||||||
dg.AddHandler(messageCreate)
|
dg.AddHandler(messageCreate)
|
||||||
err = dg.Open()
|
err = dg.Open()
|
||||||
|
|
@ -251,6 +214,14 @@ func Start() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schedule timetable checks every minute (or hour as needed)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
for range ticker.C {
|
||||||
|
checkAllUsersTimetables(dg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
fmt.Println("Bot is now running. Press CTRL+C to exit.")
|
fmt.Println("Bot is now running. Press CTRL+C to exit.")
|
||||||
// Wait for CTRL+C or other term signal to exit.
|
// Wait for CTRL+C or other term signal to exit.
|
||||||
sc := make(chan os.Signal, 1)
|
sc := make(chan os.Signal, 1)
|
||||||
|
|
@ -260,130 +231,14 @@ 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
|
|
||||||
func NotifyAllUsers() {
|
|
||||||
if DiscordSession != nil {
|
|
||||||
checkAllUsersTimetables(DiscordSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, 0o644); 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 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if m.GuildID != "" && m.Content == "!WebHook" {
|
|
||||||
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
|
||||||
WebHook = !WebHook
|
|
||||||
if WebHook {
|
|
||||||
s.ChannelMessageSend(m.ChannelID, "Sending Webhook set to True")
|
|
||||||
} else {
|
|
||||||
s.ChannelMessageSend(m.ChannelID, "Sending Webhook set to False")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Handle "!addaccount" only in guilds (not in DMs)
|
// Handle "!addaccount" only in guilds (not in DMs)
|
||||||
if m.Content == "!addaccount" {
|
if m.GuildID != "" && m.Content == "!addaccount" {
|
||||||
// Delete the command for privacy
|
// Delete the command for privacy
|
||||||
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
||||||
// Create DM channel
|
// Create DM channel
|
||||||
|
|
@ -400,35 +255,6 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Content == "!update" {
|
|
||||||
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
|
||||||
checkAllUsersTimetables(DiscordSession)
|
|
||||||
s.ChannelMessageSend(m.ChannelID, "Updated all Timetables manually")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -1,31 +1,18 @@
|
||||||
# Discord bot for logging Untis timetable to a Discord Webhook and Multiple accounts which can be added using !addaccount in a server whit the Bot
|
# Discord bot for logging Untis timetable to a Discord Webhook and Multiple accounts which can be added using !addaccount in a server whit the Bot
|
||||||
|
## create .env with Credentials:
|
||||||
## create .env with Credentials
|
|
||||||
|
|
||||||
- UNTIS_PASSWORD
|
- UNTIS_PASSWORD
|
||||||
- UNTIS_USER
|
- UNTIS_USER
|
||||||
- DISCORD_WEBHOOK_URL
|
- DISCORD_WEBHOOK_URL
|
||||||
- DISCORD_BOT_TOKEN
|
- DISCORD_BOT_TOKEN
|
||||||
- ENC_KEY (generated via head -c 32 /dev/urandom | base64)
|
- ENC_KEY (generated via head -c 32 /dev/urandom | base64)
|
||||||
- URL
|
|
||||||
- TIMEZONE (will default to UTC if empty)
|
|
||||||
|
|
||||||
### URL can be found by following this structure
|
# This is a project for me, issues will be resolved as i find the motivation to do so, improvements may follow in the future
|
||||||
|
### I will try to maintain this project as best as possible. Maybe add a better security to it than to trust the host but for now it is working and that was my goal. Please report any errors you find while using this bot.
|
||||||
"<https://[SchoolName].webuntis.com/WebUntis/jsonrpc.do?school=[SchholName]"
|
|
||||||
while SchoolName can be found by visiting the official website, going to the login for your school and it will be:
|
|
||||||
"<https://[SchoolName].webuntis.com/WebUntis/?school=[SchoolName]#/basic/login>"
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
# This is a discord application intended for self hosting. You will need to add an application in the [disord developer website](https://discord.com/developers/applications) 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
|
### 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.
|
||||||
|
|
||||||
# I also recommend to use a tool like [coolify](https://coolify.io/) to host the bot. Make sure to add the Env's and add the **accounts/** folder to persistent storage
|
## 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.
|
||||||
|
|
||||||
### 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
|
# 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.
|
||||||
|
|
||||||
### 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 just untis username). 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 and the password, created with your discord account
|
|
||||||
|
|
||||||
## For your timezone setup an ENV called "LOCATION_ENV" with your timezone as explained in the example env file. 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
|
|
||||||
113
Untis/Untis.go
113
Untis/Untis.go
|
|
@ -1,113 +0,0 @@
|
||||||
package Untis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Params struct {
|
|
||||||
Users string `json:"user"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Client string `json:"client"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Login struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params Params `json:"params"`
|
|
||||||
|
|
||||||
Jsonrpc string `json:"jsonrpc"`
|
|
||||||
}
|
|
||||||
type LoginResponse struct {
|
|
||||||
Jsonrpc string `json:"jsonrpc"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Result Loginresult `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var Url = os.Getenv("URL")
|
|
||||||
|
|
||||||
func Main(user, password, userID string) {
|
|
||||||
godotenv.Load("../.env")
|
|
||||||
cookies, err := Auth(user, password, userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Authentication failed for user %s: %v", user, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Rooms(cookies)
|
|
||||||
Classes(cookies)
|
|
||||||
Subjects(cookies)
|
|
||||||
Timetable(cookies, userID)
|
|
||||||
Teachers(cookies)
|
|
||||||
log.Printf("updated everything for user: %s ", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Update(user, password, userID string) {
|
|
||||||
godotenv.Load("../.env")
|
|
||||||
cookies, err := Auth(user, password, userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Authentication failed for user %s: %v", user, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timetable(cookies, userID)
|
|
||||||
log.Printf("Updated info for user: %s ", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Auth(user, password, userID string) ([]*http.Cookie, error) {
|
|
||||||
loginJSON, _ := json.Marshal(Login{
|
|
||||||
"2023-05-06 15:44:22.215292", "authenticate",
|
|
||||||
Params{user, password, "WebUntis Test"},
|
|
||||||
"2.0",
|
|
||||||
})
|
|
||||||
if Url == "" {
|
|
||||||
Url = os.Getenv("URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
var LoginOut *http.Response
|
|
||||||
var err error
|
|
||||||
for attempt := 1; attempt <= 3; attempt++ {
|
|
||||||
LoginOut, err = http.Post(Url, "application/json", bytes.NewReader(loginJSON))
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Printf("Auth attempt %d/3 failed for %s: %v", attempt, user, err)
|
|
||||||
time.Sleep(time.Duration(attempt*2) * time.Second)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer LoginOut.Body.Close()
|
|
||||||
cookies := LoginOut.Cookies()
|
|
||||||
// log.Printf("Login successful for user: %s", user)//log not needed
|
|
||||||
|
|
||||||
response, err := io.ReadAll(LoginOut.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var Response LoginResponse
|
|
||||||
if err := json.Unmarshal(response, &Response); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
loginFile := "login.json"
|
|
||||||
if userID != "" {
|
|
||||||
loginFile = "login_" + userID + ".json"
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(loginFile, data, 0o644); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookies, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package Untis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Location *time.Location
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
godotenv.Load("../.env")
|
|
||||||
timezone := os.Getenv("LOCATION_ENV")
|
|
||||||
if timezone == "" {
|
|
||||||
timezone = "UTC"
|
|
||||||
}
|
|
||||||
loc, err := time.LoadLocation(timezone)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to load timezone, set as UTC")
|
|
||||||
loc = time.UTC
|
|
||||||
}
|
|
||||||
Location = loc
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTime() time.Time {
|
|
||||||
return time.Now().In(Location)
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
DISCORD_WEBHOOK_URL=[Paste Webhook]
|
|
||||||
UNTIS_PASSWORD=[Default user passwd]
|
|
||||||
UNTIS_USER=[Default user username]
|
|
||||||
DISCORD_BOT_TOKEN=[Paste Bot token]
|
|
||||||
ENC_KEY=[Generate enc key and paste]
|
|
||||||
URL=[Untis url, create from tutorial in readme]
|
|
||||||
LOCATION_ENV=Europe/Berlin [change to your timezone, needs to be usable by go's time package via time.LoadLocation(LOCATION_ENV)]
|
|
||||||
8
go.mod
8
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module untislogger
|
module untislogger
|
||||||
|
|
||||||
go 1.27.0
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.29.0
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
|
|
@ -8,7 +8,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
9
go.sum
9
go.sum
|
|
@ -1,17 +1,14 @@
|
||||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
|
||||||
509
main.go
509
main.go
|
|
@ -4,18 +4,15 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
_ "time/tzdata"
|
Untis "untislogger/Bot"
|
||||||
|
|
||||||
Untis "untislogger/Untis"
|
|
||||||
|
|
||||||
BotStart "untislogger/Botrun"
|
BotStart "untislogger/Botrun"
|
||||||
|
|
||||||
|
|
@ -35,65 +32,15 @@ type NamedTimetableEntry struct {
|
||||||
ActivityType string `json:"activityType"`
|
ActivityType string `json:"activityType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var location *time.Location
|
|
||||||
|
|
||||||
// lessonKey creates a stable key for a lesson (date + time + subject)
|
|
||||||
func lessonKey(e NamedTimetableEntry) string {
|
|
||||||
subject := "unknown"
|
|
||||||
if len(e.Su) > 0 {
|
|
||||||
subject = e.Su[0]
|
|
||||||
}
|
|
||||||
// Include date for multi-day accuracy (e.g., Monday vs Tuesday)
|
|
||||||
return e.Date + "|" + e.StartTime + "|" + subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSubject returns the first subject or "Free period"
|
|
||||||
func getSubject(e NamedTimetableEntry) string {
|
|
||||||
if len(e.Su) == 0 {
|
|
||||||
return "Free period"
|
|
||||||
}
|
|
||||||
return e.Su[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatRoom returns a readable room string
|
|
||||||
func formatRoom(rooms []string) string {
|
|
||||||
if len(rooms) == 0 {
|
|
||||||
return "no room"
|
|
||||||
}
|
|
||||||
return strings.Join(rooms, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatStatus returns a human-readable status
|
|
||||||
func formatStatus(code string) string {
|
|
||||||
if code == "" {
|
|
||||||
return "regular"
|
|
||||||
}
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// slicesEqual checks if two string slices are equal (order-sensitive)
|
|
||||||
func slicesEqual(a, b []string) bool {
|
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range a {
|
|
||||||
if a[i] != b[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// init and main//
|
// init and main//
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load(".env")
|
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error loading .env file: %v", err)
|
log.Printf("Error loading .env file: %v", err)
|
||||||
}
|
}
|
||||||
discordWebhookURL = os.Getenv("DISCORD_WEBHOOK_URL")
|
discordWebhookURL = os.Getenv("DISCORD_WEBHOOK_URL")
|
||||||
log.Println("Initializing application...")
|
log.Println("Initializing application...")
|
||||||
// log.Printf("DISCORD_WEBHOOK_URL: %q", discordWebhookURL) //Debug info, not needed at the moment
|
log.Printf("DISCORD_WEBHOOK_URL: %q", discordWebhookURL)
|
||||||
|
|
||||||
// Check if Discord webhook is configured
|
// Check if Discord webhook is configured
|
||||||
if discordWebhookURL != "" {
|
if discordWebhookURL != "" {
|
||||||
|
|
@ -104,18 +51,24 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initUpdate()
|
//Untis.Main() //starting API calls function| happens in schedule func
|
||||||
|
//Run()
|
||||||
|
//Starts logging the timetable for each new Lesson and logs changes
|
||||||
go BotStart.Start()
|
go BotStart.Start()
|
||||||
scheduleTimetableUpdate()
|
scheduleTimetableUpdate()
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
log.Println("Program is running. Press Ctrl+C to stop.")
|
||||||
|
//Start logging
|
||||||
|
//Run()
|
||||||
|
// Block until we receive a signal
|
||||||
<-sigChan
|
<-sigChan
|
||||||
log.Println("Shutting down...")
|
log.Println("Shutting down...")
|
||||||
}
|
}
|
||||||
|
|
||||||
func isScheduledTime(now time.Time) bool {
|
func isScheduledTime(now time.Time) bool {
|
||||||
scheduled := []string{"07:45", "08:35", "09:35", "10:25", "11:25", "12:15", "13:40", "14:25", "15:20", "16:00"}
|
scheduled := []string{"07:45", "08:35", "09:35", "10:25", "11:25", "12:15", "13:45", "14:25"}
|
||||||
current := now.Format("15:04")
|
current := now.Format("15:04")
|
||||||
for _, t := range scheduled {
|
for _, t := range scheduled {
|
||||||
if t == current {
|
if t == current {
|
||||||
|
|
@ -124,217 +77,50 @@ func isScheduledTime(now time.Time) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func initUpdate() {
|
|
||||||
defaultUser := os.Getenv("UNTIS_USER")
|
|
||||||
defaultPassword := os.Getenv("UNTIS_PASSWORD")
|
|
||||||
Untis.Main(defaultUser, defaultPassword, "default")
|
|
||||||
}
|
|
||||||
|
|
||||||
func scheduleTimetableUpdate() {
|
func scheduleTimetableUpdate() {
|
||||||
prevData := make(map[string][]byte)
|
var prevData []byte
|
||||||
|
//declare user and pass
|
||||||
// Function to update all users
|
godotenv.Load(".env")
|
||||||
updateAllUsers := func() {
|
var password = os.Getenv("UNTIS_PASSWORD")
|
||||||
// Load all registered users
|
var user = os.Getenv("UNTIS_USER")
|
||||||
accounts, err := BotStart.LoadAndDecryptAccounts()
|
Untis.Main(user, password)
|
||||||
if err != nil {
|
// Initial read of the file
|
||||||
log.Printf("Error loading accounts: %v", err)
|
data, err := os.ReadFile("timetableFilled.json")
|
||||||
}
|
if err == nil {
|
||||||
// Add the default user from .env
|
prevData = data
|
||||||
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
|
// Ticker for checking timetable changes every hour
|
||||||
for _, acc := range accounts {
|
hourTicker := time.NewTicker(1 * time.Minute)
|
||||||
// Fetch the latest timetable and only timetable
|
|
||||||
Untis.Update(acc.Username, acc.DecryptedPassword, acc.UserID)
|
|
||||||
|
|
||||||
// Check for changes
|
|
||||||
timetableFile := fmt.Sprintf("timetableFilled_%s.json", acc.UserID)
|
|
||||||
if acc.UserID == "default" {
|
|
||||||
timetableFile = "timetableFilled_default.json"
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(timetableFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error reading timetable for user %s: %v", acc.Username, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Parse current and previous timetable data
|
|
||||||
var currentEntries, prevEntries []NamedTimetableEntry
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, ¤tEntries); err != nil {
|
|
||||||
log.Printf("Failed to parse new timetable for user %s: %v", acc.Username, err)
|
|
||||||
prevData[acc.UserID] = data // still update to avoid repeated errors
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if prev, ok := prevData[acc.UserID]; ok {
|
|
||||||
if err := json.Unmarshal(prev, &prevEntries); err != nil {
|
|
||||||
log.Printf("Failed to parse previous timetable for user %s: %v", acc.Username, err)
|
|
||||||
prevData[acc.UserID] = data
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build maps keyed by a stable lesson identifier
|
|
||||||
currMap := make(map[string]NamedTimetableEntry)
|
|
||||||
prevMap := make(map[string]NamedTimetableEntry)
|
|
||||||
// Also build timeslot-based maps for better exchange detection
|
|
||||||
currTimeslotMap := make(map[string]NamedTimetableEntry)
|
|
||||||
prevTimeslotMap := make(map[string]NamedTimetableEntry)
|
|
||||||
|
|
||||||
// Get current date to filter out new day changes
|
|
||||||
today := Untis.GetTime().Format("02-01-2006")
|
|
||||||
// Get current time to filter any changes to Timetable before 3am so the new day changes stop
|
|
||||||
//
|
|
||||||
now := Untis.GetTime()
|
|
||||||
// If before 1am stop and update userdata
|
|
||||||
if now.Hour() < 1 {
|
|
||||||
prevData[acc.UserID] = data
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range currentEntries {
|
|
||||||
key := lessonKey(e)
|
|
||||||
currMap[key] = e
|
|
||||||
}
|
|
||||||
for _, e := range prevEntries {
|
|
||||||
key := lessonKey(e)
|
|
||||||
prevMap[key] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
var changes []string
|
|
||||||
|
|
||||||
// Check for modified or new lessons
|
|
||||||
for key, curr := range currMap {
|
|
||||||
// Skip lessons that are not for today to avoid new day spam
|
|
||||||
if curr.Date != today {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if prev, exists := prevMap[key]; exists {
|
|
||||||
// Existing lesson — check for changes
|
|
||||||
subject := getSubject(curr)
|
|
||||||
|
|
||||||
// Room change?
|
|
||||||
if !slicesEqual(prev.Ro, curr.Ro) {
|
|
||||||
oldRoom := formatRoom(prev.Ro)
|
|
||||||
newRoom := formatRoom(curr.Ro)
|
|
||||||
changes = append(changes, fmt.Sprintf("Room changed for %s: %s → %s", subject, oldRoom, newRoom))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range currentEntries {
|
|
||||||
key := lessonKey(e)
|
|
||||||
currMap[key] = e
|
|
||||||
// Create timeslot key (date + startTime + endTime)
|
|
||||||
timeslotKey := e.Date + "|" + e.StartTime + "|" + e.EndTime
|
|
||||||
currTimeslotMap[timeslotKey] = e
|
|
||||||
}
|
|
||||||
for _, e := range prevEntries {
|
|
||||||
key := lessonKey(e)
|
|
||||||
prevMap[key] = e
|
|
||||||
// Create timeslot key (date + startTime + endTime)
|
|
||||||
timeslotKey := e.Date + "|" + e.StartTime + "|" + e.EndTime
|
|
||||||
prevTimeslotMap[timeslotKey] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code/status change?
|
|
||||||
if prev.Code != curr.Code {
|
|
||||||
oldStatus := formatStatus(prev.Code)
|
|
||||||
newStatus := formatStatus(curr.Code)
|
|
||||||
changes = append(changes, fmt.Sprintf("Status changed for %s: %s → %s", subject, oldStatus, newStatus))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New lesson - but check if it's actually an exchange in the same timeslot
|
|
||||||
timeslotKey := curr.Date + "|" + curr.StartTime + "|" + curr.EndTime
|
|
||||||
if prevInTimeslot, exists := prevTimeslotMap[timeslotKey]; exists {
|
|
||||||
// This is an exchange: old lesson was removed, new one added in same timeslot
|
|
||||||
oldSubject := getSubject(prevInTimeslot)
|
|
||||||
newSubject := getSubject(curr)
|
|
||||||
room := formatRoom(curr.Ro)
|
|
||||||
changes = append(changes, fmt.Sprintf("Lesson exchanged: %s → %s in %s", oldSubject, newSubject, room))
|
|
||||||
} else {
|
|
||||||
// Truly new lesson
|
|
||||||
subject := getSubject(curr)
|
|
||||||
room := formatRoom(curr.Ro)
|
|
||||||
changes = append(changes, fmt.Sprintf("New lesson: %s in %s", subject, room))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for removed lessons
|
|
||||||
for key, prev := range prevMap {
|
|
||||||
// Skip lessons that were not for today
|
|
||||||
if prev.Date != today {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists := currMap[key]; !exists {
|
|
||||||
subject := getSubject(prev)
|
|
||||||
// Check if this removal is part of an exchange (already handled above)
|
|
||||||
timeslotKey := prev.Date + "|" + prev.StartTime + "|" + prev.EndTime
|
|
||||||
if _, newExists := currTimeslotMap[timeslotKey]; !newExists {
|
|
||||||
// Only report as removed if no new lesson took its place
|
|
||||||
changes = append(changes, fmt.Sprintf("Lesson removed: %s", subject))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification if changes detected
|
|
||||||
if len(changes) > 0 {
|
|
||||||
log.Printf("Timetable changed for user %s: %d changes", acc.Username, len(changes))
|
|
||||||
message := "Your timetable has changed!\n\n" + strings.Join(changes, "\n")
|
|
||||||
BotStart.SendDM(acc.UserID, message)
|
|
||||||
|
|
||||||
// Optional: send to Discord webhook if configured and user is "default"
|
|
||||||
if acc.UserID == "default" && discordWebhookURL != "" {
|
|
||||||
sendUpdateDiscordWebhookWithDetails(changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Always update prevData
|
|
||||||
prevData[acc.UserID] = data
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial update
|
|
||||||
updateAllUsers()
|
|
||||||
|
|
||||||
// Ticker for periodic updates
|
|
||||||
hourTicker := time.NewTicker(30 * time.Minute)
|
|
||||||
go func() {
|
go func() {
|
||||||
for range hourTicker.C {
|
for range hourTicker.C {
|
||||||
updateAllUsers()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Ticker for scheduled lesson notifications
|
// Ticker for checking scheduled times every minute
|
||||||
startMinuteTicker(func() {
|
startMinuteTicker(func() {
|
||||||
now := Untis.GetTime()
|
now := time.Now()
|
||||||
if isScheduledTime(now) {
|
if isScheduledTime(now) {
|
||||||
log.Println("Scheduled time reached, running notifications...")
|
log.Println("Scheduled time reached, updating and running Run()")
|
||||||
accounts, _ := BotStart.LoadAndDecryptAccounts()
|
Untis.Main(user, password)
|
||||||
// Also notify the default user
|
log.Println("Updated now running Run()")
|
||||||
accounts = append(accounts, BotStart.DecryptedAccount{UserID: "default"})
|
Run()
|
||||||
for _, acc := range accounts {
|
log.Println("Finished running Run")
|
||||||
Run(acc.UserID)
|
|
||||||
}
|
|
||||||
log.Println("Finished running notifications.")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
log.Println("Updated all users")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMinuteTicker(f func()) {
|
func startMinuteTicker(f func()) {
|
||||||
now := Untis.GetTime()
|
now := time.Now()
|
||||||
next := now.Truncate(time.Minute).Add(time.Minute)
|
next := now.Truncate(time.Minute).Add(time.Minute)
|
||||||
time.Sleep(time.Until(next))
|
time.Sleep(time.Until(next))
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
|
@ -346,53 +132,42 @@ func startMinuteTicker(f func()) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(userID string) {
|
/*
|
||||||
timetableFile := "timetableFilled_default.json"
|
#Main Run Function
|
||||||
if userID != "default" {
|
*/
|
||||||
timetableFile = fmt.Sprintf("timetableFilled_%s.json", userID)
|
func Run() {
|
||||||
}
|
log.Println("Sending next Lesson")
|
||||||
log.Printf("Sending next lesson for user %s", userID)
|
codeByStartTime, err := MapTimeToCode("timetableFilled.json")
|
||||||
|
if err != nil {
|
||||||
// Check if the timetable file exists
|
log.Println(err)
|
||||||
if _, err := os.Stat(timetableFile); os.IsNotExist(err) {
|
|
||||||
log.Printf("Timetable file %s not found for user %s.", timetableFile, userID)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
codeByStartTime, _ := MapTimeToCode(timetableFile)
|
roomByStartTime, err := MapTimeToRoom("timetableFilled.json")
|
||||||
roomByStartTime, _ := MapTimeToRoom(timetableFile)
|
if err != nil {
|
||||||
subjectByStartTime, _ := MapTimeToSubject(timetableFile)
|
log.Fatal(err)
|
||||||
|
|
||||||
now := Untis.GetTime().Format("15:04")
|
|
||||||
|
|
||||||
nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, now)
|
|
||||||
if !foundRoom {
|
|
||||||
return // No upcoming lessons
|
|
||||||
}
|
}
|
||||||
|
subjectByStartTime, err := MapTimeToSubject("timetableFilled.json")
|
||||||
subject, foundSubject := NextSubjectForTime(subjectByStartTime, now)
|
if err != nil {
|
||||||
if !foundSubject {
|
log.Fatal(err)
|
||||||
return // Should not happen if room was found
|
|
||||||
}
|
}
|
||||||
|
now := time.Now().Format("15:04")
|
||||||
status, _ := NextCodeForTime(codeByStartTime, now)
|
nextTime, room, found := NextRoomForTime(roomByStartTime, now)
|
||||||
|
if found {
|
||||||
var message string
|
log.Println("found Room")
|
||||||
if status == "cancelled" {
|
Subject, found := NextSubjectForTime(subjectByStartTime, now)
|
||||||
message = fmt.Sprintf("Next lesson: **%s** at **%s**. is **%s**", subject, nextTime, status)
|
if found {
|
||||||
} else if status != "" && status != "cancelled" {
|
log.Println("Found Subject")
|
||||||
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**. Status: **%s**", subject, room, nextTime, status)
|
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 {
|
} else {
|
||||||
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**.", subject, room, nextTime)
|
sendDiscordWebhook(Subject, room, nextTime, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userID == "default" {
|
|
||||||
if BotStart.WebHook {
|
|
||||||
log.Println("WebHook status ", BotStart.WebHook)
|
|
||||||
sendDiscordWebhook(subject, room, nextTime, status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BotStart.SendDM(userID, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -403,10 +178,6 @@ func NextRoomForTime(roomByStartTime map[string]string, current string) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate 30 minutes from now
|
|
||||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
|
||||||
|
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
timeToStr := make(map[time.Time]string)
|
timeToStr := make(map[time.Time]string)
|
||||||
for t := range roomByStartTime {
|
for t := range roomByStartTime {
|
||||||
|
|
@ -414,32 +185,23 @@ func NextRoomForTime(roomByStartTime map[string]string, current string) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Only include times within the next 30 minutes
|
|
||||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
|
||||||
times = append(times, parsed)
|
times = append(times, parsed)
|
||||||
timeToStr[parsed] = t
|
timeToStr[parsed] = t
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(times) == 0 {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||||
// Return the first (earliest) room within the 30-minute window
|
for _, t := range times {
|
||||||
return timeToStr[times[0]], roomByStartTime[timeToStr[times[0]]], true
|
if t.After(now) {
|
||||||
|
return timeToStr[t], roomByStartTime[timeToStr[t]], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NextSubjectForTime(subjectByStartTime map[string]string, current string) (string, bool) {
|
func NextSubjectForTime(subjectByStartTime map[string]string, current string) (string, bool) {
|
||||||
layout := "15:04"
|
layout := "15:04"
|
||||||
now, err := time.Parse(layout, current)
|
now, err := time.Parse(layout, current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate 30 minutes from now
|
|
||||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
|
||||||
|
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
timeToStr := make(map[time.Time]string)
|
timeToStr := make(map[time.Time]string)
|
||||||
for t := range subjectByStartTime {
|
for t := range subjectByStartTime {
|
||||||
|
|
@ -447,32 +209,24 @@ func NextSubjectForTime(subjectByStartTime map[string]string, current string) (s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Only include times within the next 30 minutes
|
|
||||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
|
||||||
times = append(times, parsed)
|
times = append(times, parsed)
|
||||||
timeToStr[parsed] = t
|
timeToStr[parsed] = t
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(times) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||||
// Return the first (earliest) subject within the 30-minute window
|
for _, t := range times {
|
||||||
return subjectByStartTime[timeToStr[times[0]]], true
|
if t.After(now) {
|
||||||
|
return subjectByStartTime[timeToStr[t]], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NextCodeForTime(codeByStartTime map[string]string, current string) (string, bool) {
|
func NextCodeForTime(codeByStartTime map[string]string, current string) (string, bool) {
|
||||||
|
|
||||||
layout := "15:04"
|
layout := "15:04"
|
||||||
now, err := time.Parse(layout, current)
|
now, err := time.Parse(layout, current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate 30 minutes from now
|
|
||||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
|
||||||
|
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
timeToStr := make(map[time.Time]string)
|
timeToStr := make(map[time.Time]string)
|
||||||
for t := range codeByStartTime {
|
for t := range codeByStartTime {
|
||||||
|
|
@ -480,22 +234,18 @@ func NextCodeForTime(codeByStartTime map[string]string, current string) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Only include times within the next 30 minutes
|
|
||||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
|
||||||
times = append(times, parsed)
|
times = append(times, parsed)
|
||||||
timeToStr[parsed] = t
|
timeToStr[parsed] = t
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(times) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||||
// Return the first (earliest) code within the 30-minute window
|
for _, t := range times {
|
||||||
return codeByStartTime[timeToStr[times[0]]], true
|
if t.After(now) {
|
||||||
}
|
return codeByStartTime[timeToStr[t]], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
|
||||||
|
}
|
||||||
func MapTimeToRoom(path string) (map[string]string, error) {
|
func MapTimeToRoom(path string) (map[string]string, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -514,7 +264,6 @@ func MapTimeToRoom(path string) (map[string]string, error) {
|
||||||
}
|
}
|
||||||
return roomByStartTime, nil
|
return roomByStartTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MapTimeToCode(path string) (map[string]string, error) {
|
func MapTimeToCode(path string) (map[string]string, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -528,12 +277,13 @@ func MapTimeToCode(path string) (map[string]string, error) {
|
||||||
codeByStartTime := make(map[string]string)
|
codeByStartTime := make(map[string]string)
|
||||||
for _, entry := range table {
|
for _, entry := range table {
|
||||||
if len(entry.Code) > 0 {
|
if len(entry.Code) > 0 {
|
||||||
|
if _, exists := codeByStartTime[entry.StartTime]; !exists {
|
||||||
codeByStartTime[entry.StartTime] = entry.Code
|
codeByStartTime[entry.StartTime] = entry.Code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return codeByStartTime, nil
|
return codeByStartTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MapTimeToSubject(path string) (map[string]string, error) {
|
func MapTimeToSubject(path string) (map[string]string, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -580,18 +330,35 @@ type Field struct {
|
||||||
func sendDiscordWebhook(subject string, room string, nextTime string, Status string) {
|
func sendDiscordWebhook(subject string, room string, nextTime string, Status string) {
|
||||||
log.Println("Sending Discord webhook notification...")
|
log.Println("Sending Discord webhook notification...")
|
||||||
// Create a rich embed message
|
// Create a rich embed message
|
||||||
var message string
|
message := fmt.Sprintf(
|
||||||
if Status != "" {
|
|
||||||
message = fmt.Sprintf(
|
|
||||||
"Subject: %s\nRoom: %s\nStart-Time: %s\nStatus: %s",
|
"Subject: %s\nRoom: %s\nStart-Time: %s\nStatus: %s",
|
||||||
subject, room, nextTime, Status,
|
subject, room, nextTime, Status,
|
||||||
)
|
)
|
||||||
} else {
|
/*embed := Embed{
|
||||||
message = fmt.Sprintf(
|
Title: "Next Lesson ",
|
||||||
"Subject: %s\nRoom: %s\nStart-Time: %s",
|
Description: "The next lesson is starting soon: ",
|
||||||
subject, room, nextTime,
|
Color: 3066993, // Green color
|
||||||
)
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
Fields: []Field{
|
||||||
|
{
|
||||||
|
Name: "New Lesson",
|
||||||
|
// Value: fmt.Sprintf("`%s`", ip),
|
||||||
|
Value: fmt.Sprintf("Room: %s", room),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Start-Time",
|
||||||
|
Value: fmt.Sprintf("Start time: %s", nextTime),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Status",
|
||||||
|
Value: fmt.Sprintf("Status: %s", Status),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
payload := DiscordWebhookPayload{
|
payload := DiscordWebhookPayload{
|
||||||
Content: message,
|
Content: message,
|
||||||
}
|
}
|
||||||
|
|
@ -612,17 +379,40 @@ func sendDiscordWebhook(subject string, room string, nextTime string, Status str
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
log.Println("Discord webhook notification sent successfully")
|
log.Println("Discord webhook notification sent successfully")
|
||||||
} else {
|
} else {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendUpdateDiscordWebhookWithDetails(changes []string) {
|
func sendUpdateDiscordWebhook() {
|
||||||
if BotStart.WebHook {
|
log.Println("Sending Discord webhook notification...")
|
||||||
log.Println("Sending detailed Discord webhook notification...")
|
// Create a rich embed message
|
||||||
|
message := "A lesson on your timetable has changed"
|
||||||
message := "**Timetable Update**\n\n" + strings.Join(changes, "\n")
|
/*embed := Embed{
|
||||||
|
Title: "Next Lesson ",
|
||||||
|
Description: "The next lesson is starting soon: ",
|
||||||
|
Color: 3066993, // Green color
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
Fields: []Field{
|
||||||
|
{
|
||||||
|
Name: "New Lesson",
|
||||||
|
// Value: fmt.Sprintf("`%s`", ip),
|
||||||
|
Value: fmt.Sprintf("Room: %s", room),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Start-Time",
|
||||||
|
Value: fmt.Sprintf("Start time: %s", nextTime),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Status",
|
||||||
|
Value: fmt.Sprintf("Status: %s", Status),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*/
|
||||||
payload := DiscordWebhookPayload{
|
payload := DiscordWebhookPayload{
|
||||||
Content: message,
|
Content: message,
|
||||||
}
|
}
|
||||||
|
|
@ -641,10 +431,9 @@ func sendUpdateDiscordWebhookWithDetails(changes []string) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
log.Println("Detailed Discord webhook sent successfully")
|
log.Println("Discord webhook notification sent successfully")
|
||||||
} else {
|
} else {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
untislogger
Executable file
BIN
untislogger
Executable file
Binary file not shown.
Loading…
Reference in a new issue