works? #5

Merged
Jannover5000r merged 2 commits from feat/implement-timetable-fetching into main 2025-10-28 08:53:49 +01:00
4 changed files with 47 additions and 43 deletions
Showing only changes of commit b79451f77a - Show all commits

View file

@ -1,25 +1,32 @@
# 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
The URL portion should be as follows: "https://thalia.webuntis.com/WebUntis/jsonrpc.do?school=[nameOfYourSchool]"
the nameOfYourSchool can be found by accessing your WebUntis page in the browser. The link should be "https://thalia.webuntis.com/WebUntis/?school=[nameOfYourSchool]/basic/login" for the loginPage
# This is a project for me, issues will be resolved as i find the motivation to do so, improvements may follow in the future # 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.
## 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
# 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 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 **accounts.json** to persistant 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
### 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. ### 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 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. ## 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
## 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
# 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.

View file

@ -30,7 +30,7 @@ type LoginResponse struct {
Result Loginresult `json:"result"` Result Loginresult `json:"result"`
} }
var Url = "https://thalia.webuntis.com/WebUntis/jsonrpc.do?school=Mons_Tabor" var Url = os.Getenv("URL")
// var Password = os.Getenv("UNTIS_PASSWORD") // var Password = os.Getenv("UNTIS_PASSWORD")
// var USERS = os.Getenv("UNTIS_USER") // var USERS = os.Getenv("UNTIS_USER")

67
main.go
View file

@ -35,8 +35,7 @@ type NamedTimetableEntry struct {
// init and main// // init and main//
func init() { func init() {
godotenv.Load(".env") err := godotenv.Load(".env")
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)
} }
@ -138,32 +137,26 @@ func scheduleTimetableUpdate() {
updateAllUsers() updateAllUsers()
} }
}() }()
// New robust scheduler
// Ticker for scheduled lesson notifications
startMinuteTicker(func() {
now := time.Now()
if isScheduledTime(now) {
log.Println("Scheduled time reached, running notifications...")
accounts, _ := BotStart.LoadAndDecryptAccounts()
// Also notify the default user
accounts = append(accounts, BotStart.DecryptedAccount{UserID: "default"})
for _, acc := range accounts {
Run(acc.UserID)
}
log.Println("Finished running notifications.")
}
})
}
func startMinuteTicker(f func()) {
now := time.Now()
next := now.Truncate(time.Minute).Add(time.Minute)
time.Sleep(time.Until(next))
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(1 * time.Minute)
go func() { go func() {
for { for range ticker.C {
f() now := time.Now()
<-ticker.C currentTime := now.Format("15:04")
scheduledTimes := []string{"07:45", "08:35", "09:35", "10:25", "11:25", "12:15", "13:45", "14:25"}
for _, t := range scheduledTimes {
if t == currentTime {
log.Println("Scheduled time reached, running notifications...")
accounts, _ := BotStart.LoadAndDecryptAccounts()
// Also notify the default user
accounts = append(accounts, BotStart.DecryptedAccount{UserID: "default"})
for _, acc := range accounts {
Run(acc.UserID, t)
}
log.Println("Finished running notifications.")
}
}
} }
}() }()
} }
@ -173,7 +166,7 @@ func Run(userID string) {
if userID != "default" { if userID != "default" {
timetableFile = fmt.Sprintf("timetableFilled_%s.json", userID) timetableFile = fmt.Sprintf("timetableFilled_%s.json", userID)
} }
log.Printf("Sending next lesson for user %s", userID) log.Printf("Sending next lesson for user %s for time %s", userID, forTime)
// Check if the timetable file exists // Check if the timetable file exists
if _, err := os.Stat(timetableFile); os.IsNotExist(err) { if _, err := os.Stat(timetableFile); os.IsNotExist(err) {
@ -185,18 +178,18 @@ func Run(userID string) {
roomByStartTime, _ := MapTimeToRoom(timetableFile) roomByStartTime, _ := MapTimeToRoom(timetableFile)
subjectByStartTime, _ := MapTimeToSubject(timetableFile) subjectByStartTime, _ := MapTimeToSubject(timetableFile)
now := time.Now().Format("15:04") nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, forTime)
nextTime, room, foundRoom := NextRoomForTime(roomByStartTime, now)
if !foundRoom { if !foundRoom {
log.Printf("No upcoming lessons found for user %s at %s.", userID, forTime)
return // No upcoming lessons return // No upcoming lessons
} }
subject, foundSubject := NextSubjectForTime(subjectByStartTime, now) subject, foundSubject := NextSubjectForTime(subjectByStartTime, forTime)
if !foundSubject { if !foundSubject {
return // Should not happen if room was found return // Should not happen if room was found
} }
status, _ := NextCodeForTime(codeByStartTime, now) status, _ := NextCodeForTime(codeByStartTime, forTime)
var message string var message string
if status != "" { if status != "" {
@ -233,7 +226,7 @@ func NextRoomForTime(roomByStartTime map[string]string, current string) (string,
} }
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]) })
for _, t := range times { for _, t := range times {
if t.After(now) { if t.After(now) || t.Equal(now) {
return timeToStr[t], roomByStartTime[timeToStr[t]], true return timeToStr[t], roomByStartTime[timeToStr[t]], true
} }
} }
@ -258,7 +251,7 @@ func NextSubjectForTime(subjectByStartTime map[string]string, current string) (s
} }
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]) })
for _, t := range times { for _, t := range times {
if t.After(now) { if t.After(now) || t.Equal(now) {
return subjectByStartTime[timeToStr[t]], true return subjectByStartTime[timeToStr[t]], true
} }
} }
@ -283,7 +276,7 @@ func NextCodeForTime(codeByStartTime map[string]string, current string) (string,
} }
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]) })
for _, t := range times { for _, t := range times {
if t.After(now) { if t.After(now) || t.Equal(now) {
return codeByStartTime[timeToStr[t]], true return codeByStartTime[timeToStr[t]], true
} }
} }
@ -374,7 +367,11 @@ 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...") if discordWebhookURL == "" {
log.Println("Discord webhook URL not configured, skipping notification.")
return
}
log.Println("Attempting to send Discord webhook notification...")
// Create a rich embed message // Create a rich embed message
var message string var message string
if Status != "" { if Status != "" {

Binary file not shown.