Compare commits
40 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed61f8b97 | ||
|
|
218916e831 | ||
|
|
1f5f10f545 | ||
|
|
d2a84a8435 | ||
|
|
1d57e8a867 | ||
|
|
ae5b1f7b28 | ||
|
|
fb503f6a9e | ||
|
|
0de661c88f | ||
|
|
5f8f21732c | ||
|
|
5e247cc87c | ||
|
|
6cfe550bed | ||
|
|
0444051dcf | ||
|
|
7f3b8b2672 | ||
|
|
ffd83811ab | ||
|
|
9357f7381b | ||
|
|
3fc436fe93 | ||
|
|
bf0b89e024 | ||
|
|
2659237b3f | ||
|
|
6051141d91 | ||
|
|
18f953e444 | ||
|
|
a62e91b30b | ||
|
|
3bf0158260 | ||
|
|
110e4a5104 | ||
|
|
b3628700ae | ||
|
|
c9a5dd2ff5 | ||
|
|
74297067d8 | ||
|
|
c29389e8f2 | ||
|
|
6c07bd9d13 | ||
|
|
f05157916d | ||
|
|
b45ac423b0 | ||
|
|
a718e700c3 | ||
|
|
3ba05a64ce | ||
|
|
3d64a4947d | ||
|
|
c3f73b99d5 | ||
|
|
4d04da7f78 | ||
|
|
7646684136 | ||
|
|
f3636c15c9 | ||
|
|
39de95e3ce | ||
|
|
68d358e1e4 | ||
|
|
0613d88b28 |
15 changed files with 471 additions and 194 deletions
7
.exampleEnv
Normal file
7
.exampleEnv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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()]
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -6,7 +6,10 @@ teachers.json
|
|||
login.json
|
||||
timetable*.json
|
||||
timetableFilled*.json
|
||||
accounts.json
|
||||
accounts/
|
||||
login_*.json
|
||||
AGENTS.md
|
||||
untislogger
|
||||
untislogger*
|
||||
CHANGES_SUMMARY.md
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"untislogger/Untis"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"untislogger/Untis"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
|
@ -38,11 +39,13 @@ type UserState struct {
|
|||
var (
|
||||
userStates = make(map[string]*UserState) // userID -> state
|
||||
stateMutex sync.Mutex // protect userStates
|
||||
accountsFile = "accounts.json"
|
||||
accountsFile = "accounts/accounts.json"
|
||||
)
|
||||
|
||||
var encryptionKey []byte
|
||||
|
||||
var WebHook = true
|
||||
|
||||
func init() {
|
||||
godotenv.Load(".env")
|
||||
keyStr := os.Getenv("ENC_KEY")
|
||||
|
|
@ -105,6 +108,12 @@ func decrypt(encrypted string) (string, error) {
|
|||
func saveAccount(userID, username, password string) error {
|
||||
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
|
||||
if data, err := os.ReadFile(accountsFile); err == nil && len(data) > 0 {
|
||||
_ = json.Unmarshal(data, &accounts)
|
||||
|
|
@ -135,7 +144,7 @@ func saveAccount(userID, username, password string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(accountsFile, newData, 0644); err != nil {
|
||||
if err := os.WriteFile(accountsFile, newData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -143,10 +152,10 @@ func saveAccount(userID, username, password string) error {
|
|||
timetableFile := getTimetableFile(userID)
|
||||
timetableFilledFile := getTimetableFilledFile(userID)
|
||||
if _, err := os.Stat(timetableFile); os.IsNotExist(err) {
|
||||
os.WriteFile(timetableFile, []byte("[]"), 0644)
|
||||
os.WriteFile(timetableFile, []byte("[]"), 0o644)
|
||||
}
|
||||
if _, err := os.Stat(timetableFilledFile); os.IsNotExist(err) {
|
||||
os.WriteFile(timetableFilledFile, []byte("[]"), 0644)
|
||||
os.WriteFile(timetableFilledFile, []byte("[]"), 0o644)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -196,7 +205,8 @@ func checkTimetableChangesForUser(user Account, decPwd string, s *discordgo.Sess
|
|||
|
||||
// Compare and notify if changed
|
||||
if !bytes.Equal(newData, prevData) {
|
||||
sendLessonNotification(s, user.UserID, user.Username, "Your timetable has changed!")
|
||||
// The main.go now handles detailed change notifications
|
||||
// This function is kept for compatibility but no longer sends generic messages
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +224,6 @@ func checkAllUsersTimetables(s *discordgo.Session) {
|
|||
accounts := loadAllAccounts()
|
||||
for _, user := range accounts {
|
||||
decPwd, _ := decrypt(user.Password)
|
||||
// You can use decPwd to fetch the timetable for this user
|
||||
checkTimetableChangesForUser(user, decPwd, s)
|
||||
}
|
||||
}
|
||||
|
|
@ -242,14 +251,6 @@ func Start() {
|
|||
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.")
|
||||
// Wait for CTRL+C or other term signal to exit.
|
||||
sc := make(chan os.Signal, 1)
|
||||
|
|
@ -355,7 +356,7 @@ func deleteAccount(userID string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(accountsFile, newData, 0644); err != nil {
|
||||
if err := os.WriteFile(accountsFile, newData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -371,9 +372,18 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|||
if m.Author.ID == s.State.User.ID {
|
||||
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)
|
||||
if m.GuildID != "" && m.Content == "!addaccount" {
|
||||
if m.Content == "!addaccount" {
|
||||
// Delete the command for privacy
|
||||
_ = s.ChannelMessageDelete(m.ChannelID, m.ID)
|
||||
// Create DM channel
|
||||
|
|
@ -390,6 +400,12 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|||
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
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -8,18 +8,19 @@
|
|||
- DISCORD_BOT_TOKEN
|
||||
- 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: "<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>"
|
||||
### 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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 **accounts.json** to persistant storage
|
||||
# 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
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ func Classes(cookies []*http.Cookie) {
|
|||
g := getClasses{"2023-05-06 15:44:22.215292", "getKlassen", map[string]interface{}{}, "2.0"}
|
||||
ClassesJson, err := json.Marshal(g)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling login data: %v", err)
|
||||
log.Printf("Error marshaling login data: %v", err)
|
||||
return
|
||||
}
|
||||
classes := bytes.NewReader(ClassesJson)
|
||||
|
||||
prompt, err := http.NewRequest("POST", Url, classes)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creatingrequest: %v", err)
|
||||
log.Printf("Error creatingrequest: %v", err)
|
||||
return
|
||||
}
|
||||
// log.Println("prompt without extra header or cookie ", prompt)
|
||||
|
|
@ -72,15 +72,17 @@ func Classes(cookies []*http.Cookie) {
|
|||
var Response ClassesResponse
|
||||
err = json.Unmarshal(response, &Response)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling response: %v", err)
|
||||
log.Printf("Error unmarshaling response: %v", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile("classes.json", data, 0644)
|
||||
err = os.WriteFile("classes.json", data, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
log.Println("Updated Classes")
|
||||
// log.Println("Updated Classes")//dont need log anymore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ func Rooms(cookies []*http.Cookie) {
|
|||
g := getRooms{"2023-05-06 15:44:22.215292", "getRooms", map[string]interface{}{}, "2.0"}
|
||||
roomsJson, err := json.Marshal(g)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling login data: %v", err)
|
||||
log.Printf("Error marshaling login data: %v", err)
|
||||
return
|
||||
}
|
||||
rooms := bytes.NewReader(roomsJson)
|
||||
|
||||
prompt, err := http.NewRequest("POST", Url, rooms)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creatingrequest: %v", err)
|
||||
log.Printf("Error creatingrequest: %v", err)
|
||||
return
|
||||
}
|
||||
// log.Println("prompt without extra header or cookie ", prompt)
|
||||
|
|
@ -75,16 +75,19 @@ func Rooms(cookies []*http.Cookie) {
|
|||
var Response RoomsResponse
|
||||
err = json.Unmarshal(response, &Response)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling response: %v", err)
|
||||
log.Printf("Error unmarshaling response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile("rooms.json", data, 0644)
|
||||
err = os.WriteFile("rooms.json", data, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
log.Println("Updated Rooms")
|
||||
// log.Println("Updated Rooms")//dont need log anymore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ func Subjects(cookies []*http.Cookie) {
|
|||
g := getSubjects{"2023-05-06 15:44:22.215292", "getSubjects", map[string]interface{}{}, "2.0"}
|
||||
SubjectsJson, err := json.Marshal(g)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling login data: %v", err)
|
||||
log.Printf("Error marshaling login data: %v", err)
|
||||
return
|
||||
}
|
||||
subjects := bytes.NewReader(SubjectsJson)
|
||||
|
||||
prompt, err := http.NewRequest("POST", Url, subjects)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creatingrequest: %v", err)
|
||||
log.Printf("Error creatingrequest: %v", err)
|
||||
return
|
||||
}
|
||||
// log.Println("prompt without extra header or cookie ", prompt)
|
||||
|
|
@ -72,15 +72,18 @@ func Subjects(cookies []*http.Cookie) {
|
|||
var Response SubjectsResponse
|
||||
err = json.Unmarshal(response, &Response)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling response: %v", err)
|
||||
log.Printf("Error unmarshaling response: %v", err)
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile("subjects.json", data, 0644)
|
||||
err = os.WriteFile("subjects.json", data, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
log.Println("Updated Subjects")
|
||||
// log.Println("Updated Subjects")//dont need log anymore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ func Teachers(cookies []*http.Cookie) {
|
|||
g := getTeachers{"2023-05-06 15:44:22.215292", "getTeachers", map[string]interface{}{}, "2.0"}
|
||||
TeachersJson, err := json.Marshal(g)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling login data: %v", err)
|
||||
log.Printf("Error marshaling login data: %v", err)
|
||||
return
|
||||
}
|
||||
teachers := bytes.NewReader(TeachersJson)
|
||||
|
||||
prompt, err := http.NewRequest("POST", Url, teachers)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creatingrequest: %v", err)
|
||||
log.Printf("Error creatingrequest: %v", err)
|
||||
return
|
||||
}
|
||||
// log.Println("prompt without extra header or cookie ", prompt)
|
||||
|
|
@ -72,15 +72,18 @@ func Teachers(cookies []*http.Cookie) {
|
|||
var Response TeachersResponse
|
||||
err = json.Unmarshal(response, &Response)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling response: %v", err)
|
||||
log.Printf("Error unmarshaling response: %v", err)
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(Response.Result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile("teachers.json", data, 0644)
|
||||
err = os.WriteFile("teachers.json", data, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
log.Println("Updated Teachers")
|
||||
// log.Println("Updated Teachers")//dont need log anymore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimetableResponse struct {
|
||||
|
|
@ -100,7 +99,7 @@ func Timetable(cookies []*http.Cookie, userID string) {
|
|||
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"}
|
||||
timetablesJson, err := json.Marshal(g)
|
||||
if err != nil {
|
||||
|
|
@ -148,12 +147,12 @@ func Timetable(cookies []*http.Cookie, userID string) {
|
|||
if userID != "" {
|
||||
timetableFile = "timetable_" + userID + ".json"
|
||||
}
|
||||
if err := os.WriteFile(timetableFile, data, 0644); err != nil {
|
||||
if err := os.WriteFile(timetableFile, data, 0o644); err != nil {
|
||||
log.Printf("Error writing timetable file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Updated timetable for user %s", userID)
|
||||
// log.Printf("Updated timetable for user %s", userID) // dont need log anymore
|
||||
setTimetable(userID)
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +171,7 @@ func LoadIDMap(path string) (map[int]string, error) {
|
|||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func LoadTimetable(path string) ([]TimetableEntry, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
|
@ -183,11 +183,13 @@ func LoadTimetable(path string) ([]TimetableEntry, error) {
|
|||
}
|
||||
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]
|
||||
|
|
@ -195,6 +197,7 @@ func formatDate(date int) string {
|
|||
day := s[6:8]
|
||||
return fmt.Sprintf("%s-%s-%s", day, month, year)
|
||||
}
|
||||
|
||||
func setTimetable(userID string) {
|
||||
subjects, _ := LoadIDMap("subjects.json")
|
||||
rooms, _ := LoadIDMap("rooms.json")
|
||||
|
|
@ -242,9 +245,9 @@ func setTimetable(userID string) {
|
|||
if userID != "" {
|
||||
timetableFilledFile = "timetableFilled_" + userID + ".json"
|
||||
}
|
||||
if err := os.WriteFile(timetableFilledFile, data, 0644); err != nil {
|
||||
if err := os.WriteFile(timetableFilledFile, data, 0o644); err != nil {
|
||||
log.Printf("Error writing timetableFilled file: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Filled timetable for user %s", userID)
|
||||
// log.Printf("Filled timetable for user %s", userID)//log not needed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
|
@ -32,8 +33,6 @@ type LoginResponse struct {
|
|||
|
||||
var Url = os.Getenv("URL")
|
||||
|
||||
// var Password = os.Getenv("UNTIS_PASSWORD")
|
||||
// var USERS = os.Getenv("UNTIS_USER")
|
||||
func Main(user, password, userID string) {
|
||||
godotenv.Load("../.env")
|
||||
cookies, err := Auth(user, password, userID)
|
||||
|
|
@ -46,26 +45,46 @@ func Main(user, password, userID string) {
|
|||
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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
login := bytes.NewReader(loginJSON)
|
||||
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")
|
||||
}
|
||||
LoginOut, err := http.Post(Url, "application/json", login)
|
||||
|
||||
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.Printf("Login successful for user: %s", user)//log not needed
|
||||
|
||||
response, err := io.ReadAll(LoginOut.Body)
|
||||
if err != nil {
|
||||
|
|
|
|||
29
Untis/timeutil.go
Normal file
29
Untis/timeutil.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -4,4 +4,4 @@ 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=Europe/Berlin [change to your timezone, needs to be usable by go's time package via time.LoadLocation(LOCATION_ENV)]
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module untislogger
|
||||
|
||||
go 1.25.2
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
|
|
|
|||
316
main.go
316
main.go
|
|
@ -4,12 +4,13 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
|
@ -36,6 +37,53 @@ type NamedTimetableEntry struct {
|
|||
|
||||
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//
|
||||
func init() {
|
||||
godotenv.Load(".env")
|
||||
|
|
@ -45,7 +93,7 @@ func init() {
|
|||
}
|
||||
discordWebhookURL = os.Getenv("DISCORD_WEBHOOK_URL")
|
||||
log.Println("Initializing application...")
|
||||
log.Printf("DISCORD_WEBHOOK_URL: %q", discordWebhookURL)
|
||||
// log.Printf("DISCORD_WEBHOOK_URL: %q", discordWebhookURL) //Debug info, not needed at the moment
|
||||
|
||||
// Check if Discord webhook is configured
|
||||
if discordWebhookURL != "" {
|
||||
|
|
@ -53,44 +101,21 @@ func init() {
|
|||
} else {
|
||||
log.Println("No Discord webhook provided, Discord notifications will be disabled")
|
||||
}
|
||||
|
||||
locEnv := os.Getenv("LOCATION_ENV")
|
||||
|
||||
var locErr error
|
||||
|
||||
location, locErr = time.LoadLocation(locEnv)
|
||||
|
||||
if locErr != nil {
|
||||
log.Fatalf("Failed to load timezone: %v", locErr)
|
||||
}
|
||||
log.Printf("Timezone set to: %s", location)
|
||||
}
|
||||
|
||||
func getTime() time.Time {
|
||||
return time.Now().In(location)
|
||||
}
|
||||
|
||||
func main() {
|
||||
godotenv.Load(".env")
|
||||
// Untis.Main() //starting API calls function| happens in schedule func
|
||||
// Run()
|
||||
// Starts logging the timetable for each new Lesson and logs changes
|
||||
initUpdate()
|
||||
go BotStart.Start()
|
||||
scheduleTimetableUpdate()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
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
|
||||
log.Println("Shutting down...")
|
||||
}
|
||||
|
||||
func isScheduledTime(now time.Time) bool {
|
||||
scheduled := []string{"07:45", "08:35", "09:35", "10:25", "11:25", "12:15", "13:40", "14:25"}
|
||||
scheduled := []string{"07:45", "08:35", "09:35", "10:25", "11:25", "12:15", "13:40", "14:25", "15:20", "16:00"}
|
||||
current := now.Format("15:04")
|
||||
for _, t := range scheduled {
|
||||
if t == current {
|
||||
|
|
@ -100,6 +125,12 @@ func isScheduledTime(now time.Time) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func initUpdate() {
|
||||
defaultUser := os.Getenv("UNTIS_USER")
|
||||
defaultPassword := os.Getenv("UNTIS_PASSWORD")
|
||||
Untis.Main(defaultUser, defaultPassword, "default")
|
||||
}
|
||||
|
||||
func scheduleTimetableUpdate() {
|
||||
prevData := make(map[string][]byte)
|
||||
|
||||
|
|
@ -110,7 +141,6 @@ func scheduleTimetableUpdate() {
|
|||
if err != nil {
|
||||
log.Printf("Error loading accounts: %v", err)
|
||||
}
|
||||
|
||||
// Add the default user from .env
|
||||
defaultUser := os.Getenv("UNTIS_USER")
|
||||
defaultPassword := os.Getenv("UNTIS_PASSWORD")
|
||||
|
|
@ -124,8 +154,8 @@ func scheduleTimetableUpdate() {
|
|||
|
||||
// Process each user
|
||||
for _, acc := range accounts {
|
||||
// Fetch the latest timetable
|
||||
Untis.Main(acc.Username, acc.DecryptedPassword, acc.UserID)
|
||||
// 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)
|
||||
|
|
@ -137,12 +167,141 @@ func scheduleTimetableUpdate() {
|
|||
log.Printf("Error reading timetable for user %s: %v", acc.Username, err)
|
||||
continue
|
||||
}
|
||||
// Parse current and previous timetable data
|
||||
var currentEntries, prevEntries []NamedTimetableEntry
|
||||
|
||||
if prev, ok := prevData[acc.UserID]; ok && !bytes.Equal(data, prev) {
|
||||
log.Printf("Timetable has changed for user %s.", acc.Username)
|
||||
BotStart.SendDM(acc.UserID, "Your timetable has changed!")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +318,7 @@ func scheduleTimetableUpdate() {
|
|||
|
||||
// Ticker for scheduled lesson notifications
|
||||
startMinuteTicker(func() {
|
||||
now := getTime()
|
||||
now := Untis.GetTime()
|
||||
if isScheduledTime(now) {
|
||||
log.Println("Scheduled time reached, running notifications...")
|
||||
accounts, _ := BotStart.LoadAndDecryptAccounts()
|
||||
|
|
@ -171,10 +330,11 @@ func scheduleTimetableUpdate() {
|
|||
log.Println("Finished running notifications.")
|
||||
}
|
||||
})
|
||||
log.Println("Updated all users")
|
||||
}
|
||||
|
||||
func startMinuteTicker(f func()) {
|
||||
now := getTime()
|
||||
now := Untis.GetTime()
|
||||
next := now.Truncate(time.Minute).Add(time.Minute)
|
||||
time.Sleep(time.Until(next))
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
|
@ -203,7 +363,8 @@ func Run(userID string) {
|
|||
roomByStartTime, _ := MapTimeToRoom(timetableFile)
|
||||
subjectByStartTime, _ := MapTimeToSubject(timetableFile)
|
||||
|
||||
now := getTime().Format("15:04")
|
||||
now := Untis.GetTime().Format("15:04")
|
||||
|
||||
nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, now)
|
||||
if !foundRoom {
|
||||
return // No upcoming lessons
|
||||
|
|
@ -217,19 +378,22 @@ func Run(userID string) {
|
|||
status, _ := NextCodeForTime(codeByStartTime, now)
|
||||
|
||||
var message string
|
||||
if status != "" {
|
||||
if status == "cancelled" {
|
||||
message = fmt.Sprintf("Next lesson: **%s** at **%s**. is **%s**", subject, nextTime, status)
|
||||
} else if status != "" && status != "cancelled" {
|
||||
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**. Status: **%s**", subject, room, nextTime, status)
|
||||
} else {
|
||||
message = fmt.Sprintf("Next lesson: **%s** in room **%s** at **%s**.", subject, room, nextTime)
|
||||
}
|
||||
|
||||
if userID == "default" {
|
||||
// The default user still sends to the webhook
|
||||
if BotStart.WebHook {
|
||||
log.Println("WebHook status ", BotStart.WebHook)
|
||||
sendDiscordWebhook(subject, room, nextTime, status)
|
||||
} else {
|
||||
BotStart.SendDM(userID, message)
|
||||
}
|
||||
}
|
||||
BotStart.SendDM(userID, message)
|
||||
}
|
||||
|
||||
/*
|
||||
*/
|
||||
|
|
@ -239,6 +403,10 @@ func NextRoomForTime(roomByStartTime map[string]string, current string) (string,
|
|||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Calculate 30 minutes from now
|
||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
||||
|
||||
var times []time.Time
|
||||
timeToStr := make(map[time.Time]string)
|
||||
for t := range roomByStartTime {
|
||||
|
|
@ -246,24 +414,32 @@ func NextRoomForTime(roomByStartTime map[string]string, current string) (string,
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Only include times within the next 30 minutes
|
||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
||||
times = append(times, parsed)
|
||||
timeToStr[parsed] = t
|
||||
}
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
for _, t := range times {
|
||||
if t.After(now) {
|
||||
return timeToStr[t], roomByStartTime[timeToStr[t]], true
|
||||
}
|
||||
}
|
||||
|
||||
if len(times) == 0 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
// Return the first (earliest) room within the 30-minute window
|
||||
return timeToStr[times[0]], roomByStartTime[timeToStr[times[0]]], true
|
||||
}
|
||||
|
||||
func NextSubjectForTime(subjectByStartTime map[string]string, current string) (string, bool) {
|
||||
layout := "15:04"
|
||||
now, err := time.Parse(layout, current)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Calculate 30 minutes from now
|
||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
||||
|
||||
var times []time.Time
|
||||
timeToStr := make(map[time.Time]string)
|
||||
for t := range subjectByStartTime {
|
||||
|
|
@ -271,24 +447,32 @@ func NextSubjectForTime(subjectByStartTime map[string]string, current string) (s
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Only include times within the next 30 minutes
|
||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
||||
times = append(times, parsed)
|
||||
timeToStr[parsed] = t
|
||||
}
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
for _, t := range times {
|
||||
if t.After(now) {
|
||||
return subjectByStartTime[timeToStr[t]], true
|
||||
}
|
||||
}
|
||||
|
||||
if len(times) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
// Return the first (earliest) subject within the 30-minute window
|
||||
return subjectByStartTime[timeToStr[times[0]]], true
|
||||
}
|
||||
|
||||
func NextCodeForTime(codeByStartTime map[string]string, current string) (string, bool) {
|
||||
layout := "15:04"
|
||||
now, err := time.Parse(layout, current)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Calculate 30 minutes from now
|
||||
thirtyMinutesLater := now.Add(30 * time.Minute)
|
||||
|
||||
var times []time.Time
|
||||
timeToStr := make(map[time.Time]string)
|
||||
for t := range codeByStartTime {
|
||||
|
|
@ -296,18 +480,22 @@ func NextCodeForTime(codeByStartTime map[string]string, current string) (string,
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Only include times within the next 30 minutes
|
||||
if parsed.After(now) && parsed.Before(thirtyMinutesLater) {
|
||||
times = append(times, parsed)
|
||||
timeToStr[parsed] = t
|
||||
}
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
for _, t := range times {
|
||||
if t.After(now) {
|
||||
return codeByStartTime[timeToStr[t]], true
|
||||
}
|
||||
}
|
||||
|
||||
if len(times) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
// Return the first (earliest) code within the 30-minute window
|
||||
return codeByStartTime[timeToStr[times[0]]], true
|
||||
}
|
||||
|
||||
func MapTimeToRoom(path string) (map[string]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
|
@ -340,11 +528,9 @@ func MapTimeToCode(path string) (map[string]string, error) {
|
|||
codeByStartTime := make(map[string]string)
|
||||
for _, entry := range table {
|
||||
if len(entry.Code) > 0 {
|
||||
if _, exists := codeByStartTime[entry.StartTime]; !exists {
|
||||
codeByStartTime[entry.StartTime] = entry.Code
|
||||
}
|
||||
}
|
||||
}
|
||||
return codeByStartTime, nil
|
||||
}
|
||||
|
||||
|
|
@ -426,15 +612,16 @@ func sendDiscordWebhook(subject string, room string, nextTime string, Status str
|
|||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
log.Println("Discord webhook notification sent successfully")
|
||||
} else {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func sendUpdateDiscordWebhook() {
|
||||
log.Println("Sending Discord webhook notification...")
|
||||
// Create a rich embed message
|
||||
message := "A lesson on your timetable has changed"
|
||||
func sendUpdateDiscordWebhookWithDetails(changes []string) {
|
||||
if BotStart.WebHook {
|
||||
log.Println("Sending detailed Discord webhook notification...")
|
||||
|
||||
message := "**Timetable Update**\n\n" + strings.Join(changes, "\n")
|
||||
|
||||
payload := DiscordWebhookPayload{
|
||||
Content: message,
|
||||
|
|
@ -454,9 +641,10 @@ func sendUpdateDiscordWebhook() {
|
|||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
log.Println("Discord webhook notification sent successfully")
|
||||
log.Println("Detailed Discord webhook sent successfully")
|
||||
} else {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("Discord webhook failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
untislogger
BIN
untislogger
Binary file not shown.
Loading…
Reference in a new issue