From 3c3aaffaf6b7916dcad131049f5842892d52254d Mon Sep 17 00:00:00 2001 From: VaiTon Date: Wed, 4 Oct 2023 16:46:11 +0200 Subject: [PATCH] Use shared library for unibo APIs --- bot/bot.go | 14 ++--- commands/uni.go | 49 ++++++++-------- commands/uni_test.go | 66 +++++++++++++++------- go.mod | 6 +- go.sum | 6 +- json/actions.json | 36 ++++++++++-- model/controller.go | 120 +++++++++++++++++++++++---------------- model/description.go | 42 +++++++------- model/globals.go | 16 +++--- model/model.go | 20 +++++-- model/parse.go | 132 ++++++++++++++++++++----------------------- model/parse_test.go | 2 +- model/responses.go | 24 +++++--- utils/util.go | 21 ++++--- utils/util_test.go | 84 ++++++++++++++------------- 15 files changed, 362 insertions(+), 276 deletions(-) diff --git a/bot/bot.go b/bot/bot.go index 01aa6e6..24575a0 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -4,10 +4,11 @@ import ( "log" "strings" - "github.com/csunibo/informabot/model" - "github.com/csunibo/informabot/utils" tgbotapi "github.com/musianisamuele/telegram-bot-api" "golang.org/x/exp/slices" + + "github.com/csunibo/informabot/model" + "github.com/csunibo/informabot/utils" ) func StartInformaBot(token string, debug bool) { @@ -42,19 +43,18 @@ func run(bot *tgbotapi.BotAPI) { handleCommand(bot, &update) } else { // text message - for i := 0; i < len(model.Autoreplies); i++ { + for i := 0; i < len(model.AutoReplies); i++ { if strings.Contains(strings.ToLower(update.Message.Text), - strings.ToLower(model.Autoreplies[i].Text)) { + strings.ToLower(model.AutoReplies[i].Text)) { var msg tgbotapi.MessageConfig if update.Message.IsTopicMessage { msg = tgbotapi.NewThreadMessage(update.Message.Chat.ID, - update.Message.MessageThreadID, model.Autoreplies[i].Reply) + update.Message.MessageThreadID, model.AutoReplies[i].Reply) } else { msg = tgbotapi.NewMessage(update.Message.Chat.ID, - model.Autoreplies[i].Reply) + model.AutoReplies[i].Reply) } - msg.ReplyToMessageID = update.Message.MessageID utils.SendHTML(bot, msg) } diff --git a/commands/uni.go b/commands/uni.go index 05d91b7..c298ad1 100644 --- a/commands/uni.go +++ b/commands/uni.go @@ -3,11 +3,12 @@ package commands import ( "encoding/json" "fmt" - "io/ioutil" "log" - "net/http" - "sort" + "strings" "time" + + "github.com/csunibo/unibo-go/timetable" + "golang.org/x/exp/slices" ) const TIMEFORMAT = "2006-01-02T15:04:05" @@ -47,32 +48,34 @@ func (t *LezioniTime) UnmarshalJSON(data []byte) error { return nil } -// GetTimeTable returns the timetable for the Unibo url -// returns empty string if there are no lessons. -func GetTimeTable(url string) string { - resp, err := http.Get(url) +// GetTimeTable returns an HTML string containing the timetable for the given +// course on the given date. Returns an empty string if there are no lessons. +func GetTimeTable(courseType, courseName string, year int, day time.Time) (string, error) { + + interval := &timetable.Interval{Start: day, End: day} + events, err := timetable.FetchTimetable(courseType, courseName, "", year, interval) if err != nil { - log.Printf("Error getting json when requesting orario lezioni: %s\n", err) + log.Printf("Error getting timetable: %s\n", err) + return "", err } - defer resp.Body.Close() - - result := []OrarioLezioni{} - body, _ := ioutil.ReadAll(resp.Body) - json.Unmarshal(body, &result) - sort.Slice(result, func(i, j int) bool { - return (*time.Time)(&result[i].StartTime).Before((time.Time)(result[j].StartTime)) + // Sort the events by start time + slices.SortFunc(events, func(a, b timetable.Event) int { + return int(b.Start.Time.Sub(a.Start.Time).Nanoseconds()) }) - var message string = "" - for _, lezione := range result { - message += fmt.Sprintf(` πŸ•˜ %s`, lezione.Teams, lezione.Title) + - "\n" + lezione.Time + "\n" - if len(lezione.Aule) > 0 { - message += fmt.Sprintf(" 🏒 %s - %s\n", lezione.Aule[0].Edificio, lezione.Aule[0].Piano) - message += fmt.Sprintf(" πŸ“ %s\n", lezione.Aule[0].Indirizzo) + b := strings.Builder{} + for _, event := range events { + b.WriteString(fmt.Sprintf(` πŸ•˜ %s`, event.Teams, event.Title)) + b.WriteString("\n") + b.WriteString(event.Start.Format("15:04")) + b.WriteString("\n") + if len(event.Classrooms) > 0 { + // TODO(VaiTon): Re-implement this + //b.WriteString(fmt.Sprintf(" 🏒 %s - %s\n", event.Classrooms[0].Edificio, event.Aule[0].Piano)) + //b.WriteString(fmt.Sprintf(" πŸ“ %s\n", event.Classrooms[0].Indirizzo)) } } - return message + return b.String(), nil } diff --git a/commands/uni_test.go b/commands/uni_test.go index 1b1d5f8..48f3133 100644 --- a/commands/uni_test.go +++ b/commands/uni_test.go @@ -47,7 +47,7 @@ func TestLezioniTime_Format(t *testing.T) { } func TestLezioniTime_UnmarshalJSON(t *testing.T) { - var time LezioniTime + var lezioniTime LezioniTime type args struct { data []byte } @@ -59,7 +59,7 @@ func TestLezioniTime_UnmarshalJSON(t *testing.T) { }{ { name: "Not an error", - tr: &time, + tr: &lezioniTime, args: args{ data: []byte{34, 50, 48, 50, 51, 45, 48, 51, 45, 49, 51, 84, 49, 50, 58, 48, 48, 58, 48, 48, 34}, }, @@ -67,7 +67,7 @@ func TestLezioniTime_UnmarshalJSON(t *testing.T) { }, { name: "Error #1", - tr: &time, + tr: &lezioniTime, args: args{ data: []byte("A"), }, @@ -75,7 +75,7 @@ func TestLezioniTime_UnmarshalJSON(t *testing.T) { }, { name: "Error #2", - tr: &time, + tr: &lezioniTime, args: args{ data: []byte{}, }, @@ -83,7 +83,7 @@ func TestLezioniTime_UnmarshalJSON(t *testing.T) { }, { name: "Error #2", - tr: &time, + tr: &lezioniTime, args: args{ data: []byte{34}, }, @@ -102,44 +102,72 @@ func TestLezioniTime_UnmarshalJSON(t *testing.T) { func TestGetTimeTable(t *testing.T) { type args struct { - url string + courseType string + courseName string + year int + day time.Time } tests := []struct { - name string - args args - want string + name string + args args + want string + error bool }{ { name: "Weekend", args: args{ - url: "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=1&start=2023-03-11&end=2023-03-11", + courseType: "laurea", + courseName: "informatica", + year: 1, + day: time.Date(2023, 3, 11, 0, 0, 0, 0, time.UTC), }, want: "", }, { name: "Weekday", args: args{ - url: "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=1&start=2023-10-31&end=2023-10-31", + courseType: "laurea", + courseName: "informatica", + year: 1, + day: time.Date(2023, 10, 31, 0, 0, 0, 0, time.UTC), }, want: `πŸ•˜`, }, { name: "Not a valid url", args: args{ - url: "https://example.com", + courseType: "test", + courseName: "test", }, - want: "", + want: "", + error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := GetTimeTable(tt.args.url) - got = strings.ReplaceAll(got, " ", "") - want := strings.ReplaceAll(tt.want, " ", "") - want = strings.ReplaceAll(want, "\t", "") - if !strings.Contains(got, want) { - t.Errorf("GetTimeTable() = %v, want %v", got, want) + got, err := GetTimeTable(tt.args.courseType, tt.args.courseName, tt.args.year, tt.args.day) + if err != nil && !tt.error { + t.Errorf("GetTimeTable() error = %v", err) + return + } else { + got = strings.ReplaceAll(got, " ", "") + want := strings.ReplaceAll(tt.want, " ", "") + want = strings.ReplaceAll(want, "\t", "") + if !strings.Contains(got, want) { + t.Errorf("GetTimeTable() = %v, want %v", got, want) + } } }) } } +func TestWeekend(t *testing.T) { + + date := time.Date(2023, 3, 11, 0, 0, 0, 0, time.UTC) + result, err := GetTimeTable("laurea", "informatica", 1, date) + if err != nil { + t.Fatalf("Error while getting timetable: %s", err) + } + if result != "" { + t.Errorf("Expected empty string in weekend, got %s", result) + } +} diff --git a/go.mod b/go.mod index c11cb8c..b2d28b9 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,9 @@ module github.com/csunibo/informabot go 1.18 require ( + github.com/csunibo/unibo-go v0.0.6 github.com/mitchellh/mapstructure v1.5.0 github.com/musianisamuele/telegram-bot-api v0.0.4 -) - -require ( - golang.org/x/exp v0.0.0-20230809094429-853ea248256d + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index ce246ac..52a2f51 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ +github.com/csunibo/unibo-go v0.0.6 h1:7VDtXVTiwJwGveLyG7vP62aKMg/WpqSx2iJiGRaPlDU= +github.com/csunibo/unibo-go v0.0.6/go.mod h1:h2+xnccHa7x48RNB6d07bpHQ01ozw4oihgDOlvVrJ9U= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/musianisamuele/telegram-bot-api v0.0.4 h1:kQoE4Ih/rnyf2i8iCOwcN+zflx+H1A7+PhZx9Kkqppk= github.com/musianisamuele/telegram-bot-api v0.0.4/go.mod h1:f8epVo400dyxqaQpXt3la1mnhdQW25R1w3yqYT3GQc4= -golang.org/x/exp v0.0.0-20230809094429-853ea248256d h1:wu5bD43Ana/nF1ZmaLr3lW/FQeJU8CcI+Ln7yWHViXE= -golang.org/x/exp v0.0.0-20230809094429-853ea248256d/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/json/actions.json b/json/actions.json index d0094c4..39fe28b 100644 --- a/json/actions.json +++ b/json/actions.json @@ -87,7 +87,11 @@ "type": "todayLectures", "data": { "description": "Orari lezioni di oggi (1\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=1", + "course": { + "year": 1, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di oggi (1\u00b0 anno):\n", "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" } @@ -96,7 +100,11 @@ "type": "todayLectures", "data": { "description": "Orari lezioni di oggi (2\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=2", + "course": { + "year": 2, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di oggi (2\u00b0 anno):\n", "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" } @@ -105,7 +113,11 @@ "type": "todayLectures", "data": { "description": "Orari lezioni di oggi (3\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=3", + "course": { + "year": 3, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di oggi:\n", "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" } @@ -122,7 +134,11 @@ "type": "tomorrowLectures", "data": { "description": "Orari lezioni di domani (1\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=1", + "course": { + "year": 1, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di domani (1\u00b0 anno):\n", "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" } @@ -131,7 +147,11 @@ "type": "tomorrowLectures", "data": { "description": "Orari lezioni di domani (2\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=2", + "course": { + "year": 2, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di domani (2\u00b0 anno):\n", "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" } @@ -140,7 +160,11 @@ "type": "tomorrowLectures", "data": { "description": "Orari lezioni di domani (3\u00b0 anno)", - "url": "https://corsi.unibo.it/laurea/informatica/orario-lezioni/@@orario_reale_json?anno=3", + "course": { + "year": 3, + "type": "laurea", + "name": "Informatica" + }, "title": "Lezioni di domani (3\u00b0 anno):\n", "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" } diff --git a/model/controller.go b/model/controller.go index ae9300b..8cd1661 100644 --- a/model/controller.go +++ b/model/controller.go @@ -7,25 +7,27 @@ import ( "strings" "time" - "github.com/csunibo/informabot/commands" - "github.com/csunibo/informabot/utils" tgbotapi "github.com/musianisamuele/telegram-bot-api" "golang.org/x/exp/slices" + + "github.com/csunibo/informabot/commands" + "github.com/csunibo/informabot/utils" ) -func (data MessageData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { +func (data MessageData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { return makeResponseWithText(data.Text) } -func (data HelpData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - answer := "" +func (data HelpData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { + answer := strings.Builder{} for _, action := range Actions { - if description := action.Data.GetDescription(); description != "" { - answer += "/" + action.Name + " - " + description + "\n" + description := action.Data.GetDescription() + if description != "" { + answer.WriteString("/" + action.Name + " - " + description + "\n") } } - return makeResponseWithText(answer) + return makeResponseWithText(answer.String()) } func (data LookingForData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { @@ -45,7 +47,10 @@ func (data LookingForData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbot } else { Groups[chatId] = []int64{senderID} } - SaveGroups() + err := SaveGroups() + if err != nil { + log.Printf("Error [LookingForData]: %s\n", err) + } chatMembers := utils.GetChatMembers(bot, message.Chat.ID, Groups[chatId]) @@ -71,7 +76,7 @@ func (data LookingForData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbot return makeResponseWithText(resultMsg) } -func (data NotLookingForData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { +func (data NotLookingForData) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { if (message.Chat.Type != "group" && message.Chat.Type != "supergroup") || slices.Contains(Settings.LookingForBlackList, message.Chat.ID) { log.Print("Error [NotLookingForData]: not a group or blacklisted") return makeResponseWithText(data.ChatError) @@ -90,15 +95,18 @@ func (data NotLookingForData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tg msg = fmt.Sprintf(data.NotFoundError, chatTitle) } else { Groups[chatId] = append(Groups[chatId][:idx], Groups[chatId][idx+1:]...) - SaveGroups() + err := SaveGroups() + if err != nil { + log.Printf("Error [NotLookingForData]: %s\n", err) + } msg = fmt.Sprintf(data.Text, chatTitle) } return makeResponseWithText(msg) } -func (data YearlyData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - var chatTitle string = strings.ToLower(message.Chat.Title) +func (data YearlyData) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { + chatTitle := strings.ToLower(message.Chat.Title) // check if string starts with "Yearly" if strings.Contains(chatTitle, "primo") { @@ -112,13 +120,13 @@ func (data YearlyData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi. } } -func (data TodayLecturesData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - var todayTime time.Time = time.Now() - var todayString string = todayTime.Format("2006-01-02") - url := data.Url + fmt.Sprintf("&start=%s&end=%s", todayString, todayString) - log.Printf("URL: %s\n", url) +func (data TodayLecturesData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { - var response string = commands.GetTimeTable(url) + response, err := commands.GetTimeTable(data.Course.Type, data.Course.Name, data.Course.Year, time.Now()) + if err != nil { + log.Printf("Error [TodayLecturesData]: %s\n", err) + return makeResponseWithText("Bot internal Error, contact developers") // TODO(VaiTon): better error message + } var msg string if response != "" { @@ -130,13 +138,14 @@ func (data TodayLecturesData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tg return makeResponseWithText(msg) } -func (data TomorrowLecturesData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - var todayTime time.Time = time.Now() - var tomorrowTime time.Time = todayTime.AddDate(0, 0, 1) - var tomorrowString string = tomorrowTime.Format("2006-01-02") - url := data.Url + fmt.Sprintf("&start=%s&end=%s", tomorrowString, tomorrowString) +func (data TomorrowLecturesData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { + tomorrowTime := time.Now().AddDate(0, 0, 1) - var response string = commands.GetTimeTable(url) + response, err := commands.GetTimeTable(data.Course.Type, data.Course.Name, data.Course.Year, tomorrowTime) + if err != nil { + log.Printf("Error [TomorrowLecturesData]: %s\n", err) + return makeResponseWithText("Bot internal Error, contact developers") // TODO: better error message + } var msg string if response != "" { @@ -148,7 +157,7 @@ func (data TomorrowLecturesData) HandleBotCommand(bot *tgbotapi.BotAPI, message return makeResponseWithText(msg) } -func (data ListData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { +func (data ListData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { resultText := data.Header for _, item := range data.Items { @@ -162,43 +171,55 @@ func (data ListData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Me return makeResponseWithText(resultText) } -const BEGINNING_MONTH = time.September +const BeginningMonth = time.September func getCurrentAcademicYear() int { - now := time.Now() year := now.Year() - if now.Month() >= BEGINNING_MONTH { + if now.Month() >= BeginningMonth { return year } else { return year - 1 } } -func (data CourseData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - emails := strings.Join(data.Professors, "@unibo.it\n ") + "@unibo.it\n" - ternary_assignment := func(condition bool, true_value string) string { - if condition { - return true_value - } else { - return "" - } - } +func (data CourseData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { currentAcademicYear := fmt.Sprint(getCurrentAcademicYear()) - msg := ternary_assignment(data.Name != "", fmt.Sprintf("%s\n", data.Name)) + - ternary_assignment(data.Website != "", fmt.Sprintf("Sito\nOrario", currentAcademicYear, data.Website, currentAcademicYear, data.Website)+"\n") + - ternary_assignment(data.Professors != nil, fmt.Sprintf("Professori:\n %s", emails)) + - ternary_assignment(data.Name != "", fmt.Sprintf("πŸ“š Risorse (istanza principale)\n", utils.ToKebabCase(data.Name))) + - ternary_assignment(data.Name != "", fmt.Sprintf("πŸ“š Risorse (istanza di riserva 1)\n", utils.ToKebabCase(data.Name))) + - ternary_assignment(data.Name != "", fmt.Sprintf("πŸ“š Risorse (istanza di riserva 2)\n", utils.ToKebabCase(data.Name))) + - ternary_assignment(data.Name != "", fmt.Sprintf("πŸ“‚ Repository GitHub delle risorse\n", utils.ToKebabCase(data.Name))) + - ternary_assignment(data.Telegram != "", fmt.Sprintf("πŸ‘₯ Gruppo Studenti\n", data.Telegram)) - return makeResponseWithText(msg) + var b strings.Builder + + if data.Name != "" { + b.WriteString(fmt.Sprintf("%s\n", data.Name)) + } + + if data.Website != "" { + b.WriteString(fmt.Sprintf("Sito\n", + currentAcademicYear, data.Website)) + b.WriteString(fmt.Sprintf("Orario\n", + currentAcademicYear, data.Website)) + } + + if data.Professors != nil { + emails := strings.Join(data.Professors, "@unibo.it\n ") + "@unibo.it\n" + b.WriteString(fmt.Sprintf("Professori:\n %s", emails)) + } + + if data.Name != "" { + b.WriteString(fmt.Sprintf("πŸ“š Risorse (istanza principale)\n", utils.ToKebabCase(data.Name))) + b.WriteString(fmt.Sprintf("πŸ“š Risorse (istanza di riserva 1)\n", utils.ToKebabCase(data.Name))) + b.WriteString(fmt.Sprintf("πŸ“š Risorse (istanza di riserva 2)\n", utils.ToKebabCase(data.Name))) + b.WriteString(fmt.Sprintf("πŸ“‚ Repository GitHub delle risorse\n", utils.ToKebabCase(data.Name))) + } + + if data.Telegram != "" { + b.WriteString(fmt.Sprintf("πŸ‘₯ Gruppo Studenti\n", data.Telegram)) + } + + return makeResponseWithText(b.String()) } -func (data LuckData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { +func (data LuckData) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { var emojis = []string{"🎲", "🎯", "πŸ€", "⚽", "🎳", "🎰"} var noLuckGroups = []int64{-1563447632} // NOTE: better way to handle this? @@ -221,8 +242,7 @@ func (data LuckData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Me return makeResponseWithText(msg) } -func (data InvalidData) HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { +func (data InvalidData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { log.Printf("Probably a bug in the JSON action dictionary, got invalid data in command") - return makeResponseWithText("Bot internal Error, contact developers") } diff --git a/model/description.go b/model/description.go index 9da37ec..0825494 100644 --- a/model/description.go +++ b/model/description.go @@ -1,45 +1,45 @@ package model -func (d MessageData) GetDescription() string { - return d.Description +func (data MessageData) GetDescription() string { + return data.Description } -func (d HelpData) GetDescription() string { - return d.Description +func (data HelpData) GetDescription() string { + return data.Description } -func (d LookingForData) GetDescription() string { - return d.Description +func (data LookingForData) GetDescription() string { + return data.Description } -func (d NotLookingForData) GetDescription() string { - return d.Description +func (data NotLookingForData) GetDescription() string { + return data.Description } -func (d YearlyData) GetDescription() string { - return d.Description +func (data YearlyData) GetDescription() string { + return data.Description } -func (d TodayLecturesData) GetDescription() string { - return d.Description +func (data TodayLecturesData) GetDescription() string { + return data.Description } -func (d TomorrowLecturesData) GetDescription() string { - return d.Description +func (data TomorrowLecturesData) GetDescription() string { + return data.Description } -func (d ListData) GetDescription() string { - return d.Description +func (data ListData) GetDescription() string { + return data.Description } -func (d CourseData) GetDescription() string { - return d.Description +func (data CourseData) GetDescription() string { + return data.Description } -func (d LuckData) GetDescription() string { - return d.Description +func (data LuckData) GetDescription() string { + return data.Description } -func (d InvalidData) GetDescription() string { +func (data InvalidData) GetDescription() string { return "This data is invalidly parsed, please report this bug to the developer." } diff --git a/model/globals.go b/model/globals.go index 45ca9e3..db96d12 100644 --- a/model/globals.go +++ b/model/globals.go @@ -1,16 +1,18 @@ +// This file contains all the global variables of the bot, that are initialized +// with the start of the bot. +// +// This file should be here because it had circular imports with the Model (bot +// imported model, which imported bot in order to access the global variables, +// especially for the settings) + package model import ( "log" ) -// This file contains all the global variables of the bot, that are initialized -// with the start of the bot, this file should be here because it had circular -// imports with the Model (bot imported model, which imported bot in order to access -// the global variables (expecially for the settings)) - var ( - Autoreplies []AutoReply + AutoReplies []AutoReply Actions []Action MemeList []Meme Settings SettingsStruct @@ -19,7 +21,7 @@ var ( func InitGlobals() { var err error - Autoreplies, err = ParseAutoReplies() + AutoReplies, err = ParseAutoReplies() if err != nil { log.Fatalf("Error reading autoreply.json file: %s", err.Error()) } diff --git a/model/model.go b/model/model.go index 195fb6e..057e2e2 100644 --- a/model/model.go +++ b/model/model.go @@ -1,4 +1,6 @@ -// In this file we define all the structs used to parse JSON files into Go structs +// In this file we define all the structs used to parse JSON files into Go +// structs + package model import ( @@ -10,6 +12,8 @@ type DataInterface interface { GetDescription() string } +// GetActionFromType returns an Action struct with the right DataInterface, +// inferred from the commandType string func GetActionFromType(name string, commandType string) Action { var data DataInterface switch commandType { @@ -98,11 +102,17 @@ type YearlyData struct { NoYear string `json:"noYear"` } +type CourseId struct { + Type string `json:"type"` + Name string `json:"name"` + Year int `json:"year"` +} + type TodayLecturesData struct { - Description string `json:"description"` - Url string `json:"url"` - Title string `json:"title"` - FallbackText string `json:"fallbackText"` + Description string `json:"description"` + Course CourseId `json:"course"` + Title string `json:"title"` + FallbackText string `json:"fallbackText"` } type TomorrowLecturesData TodayLecturesData diff --git a/model/parse.go b/model/parse.go index ad48ebe..5503dc7 100644 --- a/model/parse.go +++ b/model/parse.go @@ -2,106 +2,101 @@ package model import ( "encoding/json" - "io/ioutil" - "log" + "errors" + "fmt" + "io" "os" + "strings" - "github.com/csunibo/informabot/utils" "github.com/mitchellh/mapstructure" "golang.org/x/exp/slices" + + "github.com/csunibo/informabot/utils" ) const groupsPath = "./json/groups.json" -func ParseAutoReplies() ([]AutoReply, error) { - jsonFile, err := os.Open("./json/autoreply.json") +func ParseAutoReplies() (autoReplies []AutoReply, err error) { + file, err := os.Open("./json/autoreply.json") if err != nil { - log.Println(err) - return nil, err + return nil, fmt.Errorf("error reading autoreply.json file: %w", err) } - defer jsonFile.Close() - - byteValue, _ := ioutil.ReadAll(jsonFile) - var autoreplies []AutoReply - json.Unmarshal(byteValue, &autoreplies) + err = json.NewDecoder(file).Decode(&autoReplies) + if err != nil { + return nil, fmt.Errorf("error parsing autoreply.json file: %w", err) + } - return autoreplies, nil + return } -func ParseSettings() (SettingsStruct, error) { - jsonFile, err := os.Open("./json/settings.json") +func ParseSettings() (settings SettingsStruct, err error) { + file, err := os.Open("./json/settings.json") if err != nil { - log.Println(err) - return SettingsStruct{}, err + return SettingsStruct{}, fmt.Errorf("error reading settings.json file: %w", err) } - defer jsonFile.Close() - byteValue, _ := ioutil.ReadAll(jsonFile) + err = json.NewDecoder(file).Decode(&settings) + if err != nil { + return SettingsStruct{}, fmt.Errorf("error parsing settings.json file: %w", err) + } - var settings SettingsStruct - json.Unmarshal(byteValue, &settings) + err = file.Close() + if err != nil { + return SettingsStruct{}, fmt.Errorf("error closing settings.json file: %w", err) + } - return settings, nil + return } -func ParseActions() ([]Action, error) { - jsonFile, err := os.Open("./json/actions.json") +func ParseActions() (actions []Action, err error) { + byteValue, err := os.Open("./json/actions.json") + if err != nil { + return nil, fmt.Errorf("error reading actions.json file: %w", err) + } + + actions, err = ParseActionsBytes(byteValue) if err != nil { - log.Println(err) - return nil, err + return nil, fmt.Errorf("error parsing actions.json file: %w", err) } - defer jsonFile.Close() - byteValue, _ := ioutil.ReadAll(jsonFile) - return ParseActionsBytes(byteValue) + return } -func ParseActionsBytes(bytes []byte) ([]Action, error) { +func ParseActionsBytes(reader io.Reader) (actions []Action, err error) { var mapData map[string]interface{} - err := json.Unmarshal(bytes, &mapData) + + err = json.NewDecoder(reader).Decode(&mapData) if err != nil { - log.Println(err) + return } - var actions []Action for key, value := range mapData { action := GetActionFromType(key, value.(map[string]interface{})["type"].(string)) - err := mapstructure.Decode(value, &action) + err = mapstructure.Decode(value, &action) if err != nil { - return nil, err + return } actions = append(actions, action) } - slices.SortFunc(actions, func(a, b Action) int { - if a.Name < b.Name { - return -1 - } else if a.Name > b.Name { - return 1 - } else { - return 0 - } - }) - - return actions, nil + slices.SortFunc(actions, func(a, b Action) int { return strings.Compare(a.Name, b.Name) }) + return } -func ParseMemeList() ([]Meme, error) { - jsonFile, err := os.Open("./json/memes.json") +func ParseMemeList() (memes []Meme, err error) { + byteValue, err := os.Open("./json/memes.json") if err != nil { - log.Println(err) - return nil, err + return nil, fmt.Errorf("error reading memes.json file: %w", err) } - defer jsonFile.Close() - - byteValue, _ := ioutil.ReadAll(jsonFile) var mapData map[string]interface{} - json.Unmarshal(byteValue, &mapData) + err = json.NewDecoder(byteValue).Decode(&mapData) + if err != nil { + return nil, fmt.Errorf("error parsing memes.json file: %w", err) + } - var memes []Meme for key, value := range mapData { meme := Meme{ Name: key, @@ -110,24 +105,23 @@ func ParseMemeList() ([]Meme, error) { memes = append(memes, meme) } - return memes, nil + return } func ParseOrCreateGroups() (GroupsStruct, error) { - jsonFile, err := os.Open(groupsPath) - if err != nil { - jsonFile, err = os.Create(groupsPath) - if err != nil { - log.Println(err) - return nil, err - } + byteValue, err := os.ReadFile(groupsPath) + if errors.Is(err, os.ErrNotExist) { + _, _ = os.Create(groupsPath) + return make(GroupsStruct), nil + } else if err != nil { + return nil, fmt.Errorf("error reading groups.json file: %w", err) } - defer jsonFile.Close() - - byteValue, _ := ioutil.ReadAll(jsonFile) var groups GroupsStruct - json.Unmarshal(byteValue, &groups) + err = json.Unmarshal(byteValue, &groups) + if err != nil { + return nil, fmt.Errorf("error parsing groups.json file: %w", err) + } if groups == nil { groups = make(GroupsStruct) @@ -136,6 +130,4 @@ func ParseOrCreateGroups() (GroupsStruct, error) { return groups, nil } -func SaveGroups() error { - return utils.WriteJSONFile(groupsPath, Groups) -} +func SaveGroups() error { return utils.WriteJSONFile(groupsPath, Groups) } diff --git a/model/parse_test.go b/model/parse_test.go index f1f2792..155463b 100644 --- a/model/parse_test.go +++ b/model/parse_test.go @@ -17,7 +17,7 @@ func TestActions(t *testing.T) { actions, err := ParseActionsBytes(bytes) t.Log(actions) if err != nil { - t.Error(err) + t.Fatal(err) } if len(actions) != 1 { diff --git a/model/responses.go b/model/responses.go index 444aab9..fa7961c 100644 --- a/model/responses.go +++ b/model/responses.go @@ -1,14 +1,15 @@ -// This file contains the structure and functions used to pass information +// Package model contains the structure and functions used to pass information // between the computations of the bot and the driver code, in bot.go package model -// This struct is returned by the command handler, it contains information about the -// command computation. +// CommandResponse is returned by the command handler, it contains information +// about the command computation. type CommandResponse struct { Text string NextCommand string } +// makeResponse creates a CommandResponse with the given text and nextCommand func makeResponse(text string, nextCommand string) CommandResponse { return CommandResponse{ Text: text, @@ -16,22 +17,27 @@ func makeResponse(text string, nextCommand string) CommandResponse { } } +// makeResponseWithText creates a CommandResponse with the given text (and no nextCommand) func makeResponseWithText(text string) CommandResponse { return makeResponse(text, "") } +// makeResponseWithNextCommand creates a CommandResponse with the given nextCommand (and no text) func makeResponseWithNextCommand(nextCommand string) CommandResponse { return makeResponse("", nextCommand) } -func (respose CommandResponse) IsEmpty() bool { - return respose.Text == "" && respose.NextCommand == "" +// IsEmpty returns true if the CommandResponse has no text and no nextCommand +func (r CommandResponse) IsEmpty() bool { + return r.Text == "" && r.NextCommand == "" } -func (response CommandResponse) HasText() bool { - return response.Text != "" +// HasText returns true if the CommandResponse has some text +func (r CommandResponse) HasText() bool { + return r.Text != "" } -func (response CommandResponse) HasNextCommand() bool { - return response.NextCommand != "" +// HasNextCommand returns true if the CommandResponse has some nextCommand +func (r CommandResponse) HasNextCommand() bool { + return r.NextCommand != "" } diff --git a/utils/util.go b/utils/util.go index 5d469bb..3e8d300 100644 --- a/utils/util.go +++ b/utils/util.go @@ -17,13 +17,10 @@ func SendHTML(bot *tgbotapi.BotAPI, msg tgbotapi.MessageConfig) { bot.Send(msg) } -/* convert a string into kebab case - * useful for GitHub repository - * - * example: - * string = "Logica per l'informatica" - * converted_string = ToKebabCase(string); = "logica-per-informatica" (sic!) - */ +/* +ToKebabCase convert a string into kebab case. Useful for GitHub repository +names. +*/ func ToKebabCase(str string) string { // normalize the string to NFD form normalizedStr := norm.NFD.String(strings.ToLower(strings.TrimSpace(str))) @@ -48,9 +45,15 @@ func WriteJSONFile(filename string, data interface{}) error { if err != nil { return err } - defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") - return encoder.Encode(data) + + err = encoder.Encode(data) + if err != nil { + return err + } + + err = file.Close() + return err } diff --git a/utils/util_test.go b/utils/util_test.go index e6a38aa..09cfb8a 100644 --- a/utils/util_test.go +++ b/utils/util_test.go @@ -1,55 +1,53 @@ package utils import ( + "fmt" "testing" ) -func TestToKebabCase(t *testing.T) { - // Test strings - str1 := "Logica per l'informatica" - str2 := "Informatica e Societa" - str3 := "Sistemi Operativi" - - // Expected results - exp1 := "logica-per-informatica" - exp2 := "informatica-e-societa" - exp3 := "sistemi-operativi" - - // Test - res1 := ToKebabCase(str1) - res2 := ToKebabCase(str2) - res3 := ToKebabCase(str3) - - // Check results - - if res1 != exp1 { - t.Errorf("Expected %s, got %s", exp1, res1) - } - - if res2 != exp2 { - t.Errorf("Expected %s, got %s", exp2, res2) - } - - if res3 != exp3 { - t.Errorf("Expected %s, got %s", exp3, res3) - } +func ExampleToKebabCase() { + fmt.Println(ToKebabCase("Hello World")) + // Output: hello-world } -func TestAccents(t *testing.T) { - str := "Informatica e SocietΓ " - exp := "informatica-e-societa" - - str2 := "Γ  Γ¨ Γ¬ Γ² ΓΉ" - exp2 := "a-e-i-o-u" - - res := ToKebabCase(str) - res2 := ToKebabCase(str2) +func TestToKebabCase(t *testing.T) { - if res != exp { - t.Errorf("Expected %s, got %s", exp, res) + tests := []struct { + name string + args string + want string + }{ + { + "Standard", + "Logica per l'informatica", + "logica-per-informatica", + }, + { + "Standard", + "Informatica e Societa", + "informatica-e-societa", + }, + { + "Standard", + "Sistemi Operativi", + "sistemi-operativi", + }, + { + "Accents", + "Informatica e SocietΓ ", + "informatica-e-societa", + }, + { + "Accents", + "Γ  Γ¨ Γ¬ Γ² ΓΉ", + "a-e-i-o-u", + }, } - - if res2 != exp2 { - t.Errorf("Expected %s, got %s", exp2, res2) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToKebabCase(tt.args); got != tt.want { + t.Errorf("ToKebabCase() = %v, want %v", got, tt.want) + } + }) } }