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