diff --git a/bot/bot.go b/bot/bot.go index 1dd891e..35fabc6 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -35,6 +35,18 @@ func run(bot *tgbotapi.BotAPI) { for update := range updates { if update.Message == nil { + callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data) + if _, err := bot.Request(callback); err != nil { + log.Printf("Error [bot.Request() for the callback]: %s\n", err) + continue + } + + callback_text := update.CallbackQuery.Data + + if strings.HasPrefix(callback_text, "lectures_") { + handleCallback(bot, &update, "lezioni", callback_text) + } + continue } else if filterMessage(bot, update.Message) { continue @@ -229,6 +241,15 @@ func executeCommand(bot *tgbotapi.BotAPI, update *tgbotapi.Update, commandIndex if newCommand.HasNextCommand() { handleAction(bot, update, newCommand.NextCommand) } + + if newCommand.HasRows() { + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + msg.ReplyMarkup = newCommand.Rows + if _, err := bot.Send(msg); err != nil { + msg = tgbotapi.NewMessage(update.Message.Chat.ID, "Error sending data") + bot.Send(msg) + } + } } } @@ -247,6 +268,21 @@ func handleAction(bot *tgbotapi.BotAPI, update *tgbotapi.Update, commandName str return false } +// Handle a callback searching a the good action +func handleCallback(bot *tgbotapi.BotAPI, update *tgbotapi.Update, commandName string, callback_text string) bool { + idx := slices.IndexFunc(model.Actions, func(action model.Action) bool { + return action.Name == commandName + }) + + if idx != -1 { + model.Actions[idx].Data.HandleBotCallback(bot, update, callback_text) + + return true + } + + return false +} + func filterMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message) bool { if message.Dice != nil { // msg := tgbotapi.NewMessage(message.Chat.ID, "Found a dice") diff --git a/commands/uni.go b/commands/uni.go index d860b67..98cc03b 100644 --- a/commands/uni.go +++ b/commands/uni.go @@ -50,10 +50,10 @@ func (t *LezioniTime) UnmarshalJSON(data []byte) error { // 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) { +func GetTimeTable(courseType, courseName string, curriculum string, year int, day time.Time) (string, error) { interval := &timetable.Interval{Start: day, End: day} - events, err := timetable.FetchTimetable(courseType, courseName, "", year, interval) + events, err := timetable.FetchTimetable(courseType, courseName, curriculum, year, interval) if err != nil { log.Printf("Error getting timetable: %s\n", err) return "", err diff --git a/commands/uni_test.go b/commands/uni_test.go index 48f3133..e0c2507 100644 --- a/commands/uni_test.go +++ b/commands/uni_test.go @@ -106,6 +106,7 @@ func TestGetTimeTable(t *testing.T) { courseName string year int day time.Time + curriculum string } tests := []struct { name string @@ -145,7 +146,7 @@ func TestGetTimeTable(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetTimeTable(tt.args.courseType, tt.args.courseName, tt.args.year, tt.args.day) + got, err := GetTimeTable(tt.args.courseType, tt.args.courseName, tt.args.curriculum, tt.args.year, tt.args.day) if err != nil && !tt.error { t.Errorf("GetTimeTable() error = %v", err) return @@ -163,7 +164,7 @@ func TestGetTimeTable(t *testing.T) { func TestWeekend(t *testing.T) { date := time.Date(2023, 3, 11, 0, 0, 0, 0, time.UTC) - result, err := GetTimeTable("laurea", "informatica", 1, date) + result, err := GetTimeTable("laurea", "informatica", "", 1, date) if err != nil { t.Fatalf("Error while getting timetable: %s", err) } diff --git a/json/actions.json b/json/actions.json index d13becf..11e9b2b 100644 --- a/json/actions.json +++ b/json/actions.json @@ -91,98 +91,12 @@ "text": "Lauree Triennali\n\nInformatica\n1\ufe0f\u20e3 1\u00b0 anno (Telegram, Discord)\n2\ufe0f\u20e3 2\u00b0 anno (Telegram, Discord)\n3\ufe0f\u20e3 3\u00b0 anno (Telegram, Discord)\n3\ufe0f\u20e3 Ex-3\u00b0 anno (Telegram, Discord)\n\ud83d\uddff Subreddit\n\nIngegneria Informatica\n1\ufe0f\u20e32\ufe0f\u20e33\ufe0f\u20e3 Tutti gli anni(Whatsapp)\n4\ufe0f\u20e3 Ex-3\u00b0 anno(Telegram)\n\nIngegneria e Science informatiche (Cesena)\n1\ufe0f\u20e32\ufe0f\u20e33\ufe0f\u20e3 Tutti gli anni(Whatsapp)\n\nInformatica per il Management\n1\ufe0f\u20e32\ufe0f\u20e33\ufe0f\u20e3 Tutti gli anni(Telegram)\n\nLauree Magistrali\n\nInformatica\n1\ufe0f\u20e32\ufe0f\u20e3 Tutti gli anni(Telegram)\n\nArtificial Intelligence\n1\ufe0f\u20e3 1\u00b0 anno (Telegram)\n2\ufe0f\u20e3 2\u00b0 anno (Telegram)\n\nIngegneria e Science informatiche (Cesena)\n1\ufe0f\u20e32\ufe0f\u20e3 Tutti gli anni (Whatsapp, Telegram)\n\n\ud83d\udd22 Tutti i gruppi Unibo" } }, - "lezionioggi": { - "type": "yearly", + "lezioni": { + "type": "buttonsLecture", "data": { - "description": "Orari lezioni di oggi (tuo anno)", - "command": "lezionioggi", - "noYear": "Questo non sembra essere un gruppo generale per nessun anno di corso. Per usare questo comando da qui, devi specificare /lezionioggi1, /lezionioggi2 o /lezionioggi3." - } - }, - "lezionioggi1": { - "type": "todayLectures", - "data": { - "description": "Orari lezioni di oggi (1\u00b0 anno)", - "course": { - "year": 1, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di oggi (1\u00b0 anno):\n", - "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" - } - }, - "lezionioggi2": { - "type": "todayLectures", - "data": { - "description": "Orari lezioni di oggi (2\u00b0 anno)", - "course": { - "year": 2, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di oggi (2\u00b0 anno):\n", - "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" - } - }, - "lezionioggi3": { - "type": "todayLectures", - "data": { - "description": "Orari lezioni di oggi (3\u00b0 anno)", - "course": { - "year": 3, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di oggi:\n", - "fallbackText": "Non ci sono lezioni oggi. SMETTILA DI PRESSARMI" - } - }, - "lezionidomani": { - "type": "yearly", - "data": { - "description": "Orari lezioni di domani (tuo anno)", - "command": "lezionidomani", - "noYear": "Questo non sembra essere un gruppo generale per nessun anno di corso. Per usare questo comando da qui, devi specificare /lezionidomani1, /lezionidomani2 o /lezionidomani3." - } - }, - "lezionidomani1": { - "type": "tomorrowLectures", - "data": { - "description": "Orari lezioni di domani (1\u00b0 anno)", - "course": { - "year": 1, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di domani (1\u00b0 anno):\n", - "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" - } - }, - "lezionidomani2": { - "type": "tomorrowLectures", - "data": { - "description": "Orari lezioni di domani (2\u00b0 anno)", - "course": { - "year": 2, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di domani (2\u00b0 anno):\n", - "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" - } - }, - "lezionidomani3": { - "type": "tomorrowLectures", - "data": { - "description": "Orari lezioni di domani (3\u00b0 anno)", - "course": { - "year": 3, - "type": "laurea", - "name": "informatica" - }, - "title": "Lezioni di domani (3\u00b0 anno):\n", - "fallbackText": "Non ci sono lezioni domani. SMETTILA DI PRESSARMI" + "description": "Orari lezioni", + "title": " Lezioni di %s di (%d\u00b0 anno) di giorno %s", + "fallbackText": "Non ci sono lezioni in questo giorno. SMETTILA DI PRESSARMI" } }, "materiali": { diff --git a/model/callback.go b/model/callback.go new file mode 100644 index 0000000..ceeabaf --- /dev/null +++ b/model/callback.go @@ -0,0 +1,123 @@ +package model + +import ( + "fmt" + "log" + "regexp" + "strconv" + "strings" + "time" + + tgbotapi "github.com/musianisamuele/telegram-bot-api" + + "github.com/csunibo/informabot/commands" +) + +func (_ MessageData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `MessageData`") +} + +func (_ HelpData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `HelpData`") +} + +func (_ IssueData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `IssueData`") +} + +func (_ LookingForData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `LookingForData`") +} + +func (_ NotLookingForData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `NotLookingForData`") +} + +// Handle the callback for the lectures command (`/lezioni`) +// Parse the `callback_text` to check which operation it must to do: +// - If the string ends with "_today" or "_tomorrow" it returns the timetable +// - If the string just contains a "_y_" it asks for today or tomorrow +// - Otherwise prints the course year of what timetable the user wants to see +func (data Lectures) HandleBotCallback(bot *tgbotapi.BotAPI, update *tgbotapi.Update, callback_text string) { + var chatId = int64(update.CallbackQuery.Message.Chat.ID) + var messageId = update.CallbackQuery.Message.MessageID + + if strings.Contains(callback_text, "_day_") { + dayRegex, err := regexp.Compile(`_day_(\d+)`) + if err != nil { + log.Printf("Error [dayRegex]: %s\n", err) + return + } + + unixTime, err := strconv.ParseInt(dayRegex.FindString(callback_text)[5:], 10, 64) + if err != nil { + log.Printf("Error [unixTime]: %s\n", err) + return + } + + timeForLectures := time.Unix(unixTime, 0) + + yearRegex, err := regexp.Compile(`_y_(\d)_`) + if err != nil { + log.Printf("Error [yearRegex]: %s\n", err) + return + } + + year, err := strconv.Atoi(yearRegex.FindString(callback_text)[3:4]) + if err != nil { + log.Printf("Error [convert to integer the year regex]: %s\n", err) + return + } + + timetableKey := callback_text[len("lectures_"):strings.Index(callback_text, "_y_")] + + timetable := Timetables[timetableKey] + response, err := commands.GetTimeTable(timetable.Type, timetable.Name, timetable.Curriculum, year, timeForLectures) + if err != nil { + log.Printf("Error [GetTimeTable]: %s\n", err) + } + + if response == "" { + response = data.FallbackText + } else { + response = fmt.Sprintf(data.Title, timetable.Course, year, timeForLectures.Format("2006-01-02")) + "\n\n" + response + } + + editConfig := tgbotapi.NewEditMessageText(chatId, messageId, response) + editConfig.ParseMode = tgbotapi.ModeHTML + + _, err = bot.Send(editConfig) + if err != nil { + log.Printf("Error [bot.Send() for the NewEditMessageText]: %s\n", err) + } + } else if strings.Contains(callback_text, "_y_") { + rows := ChooseTimetableDay(callback_text) + keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) + editConfig := tgbotapi.NewEditMessageReplyMarkup(chatId, messageId, keyboard) + _, err := bot.Send(editConfig) + if err != nil { + log.Printf("Error [bot.Send() for the NewEditMessageReplyMarkup]: %s\n", err) + } + } else { + timetableName := strings.TrimPrefix(callback_text, "lectures_") + rows := GetLectureYears(callback_text, Timetables[timetableName].Course) + keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) + editConfig := tgbotapi.NewEditMessageReplyMarkup(chatId, messageId, keyboard) + _, err := bot.Send(editConfig) + if err != nil { + log.Printf("Error [bot.Send() for the NewEditMessageReplyMarkup]: %s\n", err) + } + } +} + +func (_ ListData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `ListData`") +} + +func (_ LuckData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `LuckData`") +} + +func (_ InvalidData) HandleBotCallback(_bot *tgbotapi.BotAPI, _udpate *tgbotapi.Update, _callback_text string) { + log.Printf("`HandleBotCallback` not defined for `InvalidData`") +} diff --git a/model/controller.go b/model/controller.go index d59d031..f9cb8fb 100644 --- a/model/controller.go +++ b/model/controller.go @@ -10,7 +10,6 @@ import ( tgbotapi "github.com/musianisamuele/telegram-bot-api" "golang.org/x/exp/slices" - "github.com/csunibo/informabot/commands" "github.com/csunibo/informabot/utils" ) @@ -144,59 +143,10 @@ func (data NotLookingForData) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbo return makeResponseWithText(msg) } -func (data YearlyData) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { - chatTitle := strings.ToLower(message.Chat.Title) - - // check if string contains the year number - if strings.Contains(chatTitle, "primo") || - strings.Contains(chatTitle, "first") { - return makeResponseWithNextCommand(data.Command + "1") - } else if strings.Contains(chatTitle, "secondo") || - strings.Contains(chatTitle, "second") { - return makeResponseWithNextCommand(data.Command + "2") - } else if strings.Contains(chatTitle, "terzo") || - strings.Contains(chatTitle, "third") { - return makeResponseWithNextCommand(data.Command + "3") - } else { - return makeResponseWithText(data.NoYear) - } -} - -func (data TodayLecturesData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { - - 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") - } - - var msg string - if response != "" { - msg = data.Title + response - } else { - msg = data.FallbackText - } - - return makeResponseWithText(msg) -} - -func (data TomorrowLecturesData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { - tomorrowTime := time.Now().AddDate(0, 0, 1) - - 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") - } - - var msg string - if response != "" { - msg = data.Title + response - } else { - msg = data.FallbackText - } - - return makeResponseWithText(msg) +func (data Lectures) HandleBotCommand(_ *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse { + rows := GetTimetableCoursesRows(&Timetables) + keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) + return makeResponseWithInlineKeyboard(keyboard) } func (data ListData) HandleBotCommand(*tgbotapi.BotAPI, *tgbotapi.Message) CommandResponse { diff --git a/model/description.go b/model/description.go index c8e988f..1fc7548 100644 --- a/model/description.go +++ b/model/description.go @@ -20,15 +20,7 @@ func (d NotLookingForData) GetDescription() string { return d.Description } -func (d YearlyData) GetDescription() string { - return d.Description -} - -func (d TodayLecturesData) GetDescription() string { - return d.Description -} - -func (d TomorrowLecturesData) GetDescription() string { +func (d Lectures) GetDescription() string { return d.Description } diff --git a/model/globals.go b/model/globals.go index cb4502b..94fa601 100644 --- a/model/globals.go +++ b/model/globals.go @@ -19,6 +19,7 @@ var ( Settings SettingsStruct Teachings map[string]Teaching Groups GroupsStruct + Timetables map[string]Timetable Mantainers []Mantainer ) @@ -59,9 +60,13 @@ func InitGlobals() { log.Fatalf("Error reading or creating groups.json file: %s", err.Error()) } + Timetables, err = ParseTimetables() + if err != nil { + log.Fatalf(err.Error()) + } + Mantainers, err = ParseMantainers() if err != nil { log.Fatalf("Error parsing mantainers.json file: %s", err.Error()) } - } diff --git a/model/model.go b/model/model.go index e89c90e..4e98815 100644 --- a/model/model.go +++ b/model/model.go @@ -9,6 +9,7 @@ import ( type DataInterface interface { HandleBotCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) CommandResponse + HandleBotCallback(bot *tgbotapi.BotAPI, update *tgbotapi.Update, callback_text string) GetDescription() string } @@ -27,12 +28,8 @@ func GetActionFromType(name string, commandType string) Action { data = LookingForData{} case "notLookingFor": data = NotLookingForData{} - case "yearly": - data = YearlyData{} - case "todayLectures": - data = TodayLecturesData{} - case "tomorrowLectures": - data = TomorrowLecturesData{} + case "buttonsLecture": + data = Lectures{} case "list": data = ListData{} case "luck": @@ -107,6 +104,23 @@ type Degree struct { Chat string `json:"chat"` } +// timetables.json + +type Curriculum struct { + Name string `json:"name"` + Callback string `json:"callback"` +} + +// Recognized by a callback string +type Timetable struct { + Course string `json:"course"` // Course title + Name string `json:"name"` // Course name + Type string `json:"type"` // Type (laurea|magistrale|2cycle) + Curriculum string `json:"curricula"` // Curriculum + Title string `json:"title"` + FallbackText string `json:"fallbackText"` +} + // SECTION ACTION STRUCTS DATA type MessageData struct { Text string `json:"text"` @@ -138,27 +152,12 @@ type NotLookingForData struct { NotFoundError string `json:"notFoundError"` } -type YearlyData struct { - Description string `json:"description"` - Command string `json:"command"` - 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"` - Course CourseId `json:"course"` - Title string `json:"title"` - FallbackText string `json:"fallbackText"` +type Lectures struct { + Description string `json:"description"` + Title string `json:"title"` + FallbackText string `json:"fallbackText"` } -type TomorrowLecturesData TodayLecturesData - type ListData struct { Description string `json:"description"` Header string `json:"header"` diff --git a/model/parse.go b/model/parse.go index 263d1d5..6b856cf 100644 --- a/model/parse.go +++ b/model/parse.go @@ -16,11 +16,12 @@ import ( ) const ( - jsonPath = "./json/" - groupsFile = "groups.json" - configSubpath = "config/" - degreesFile = "degrees.json" - teachingsFile = "teachings.json" + jsonPath = "./json/" + groupsFile = "groups.json" + configSubpath = "config/" + degreesFile = "degrees.json" + teachingsFile = "teachings.json" + timetablesFile = "timetables.json" ) func ParseAutoReplies() (autoReplies []AutoReply, err error) { @@ -207,6 +208,25 @@ func ParseOrCreateGroups() (GroupsStruct, error) { func SaveGroups(groups GroupsStruct) error { return utils.WriteJSONFile(groupsFile, groups) } +func ParseTimetables() (timetables map[string]Timetable, err error) { + filepath := filepath.Join(jsonPath, configSubpath, timetablesFile) + file, err := os.Open(filepath) + defer file.Close() + if err != nil { + return nil, fmt.Errorf("error reading %s file: %w", timetablesFile, err) + } + + var mapData map[string]Timetable + + err = json.NewDecoder(file).Decode(&mapData) + if err != nil { + return nil, fmt.Errorf("error parsing %s file: %w", filepath, err) + } + + timetables = mapData + return +} + func ParseMantainers() (mantainer []Mantainer, err error) { file, err := os.ReadFile("./json/config/mantainers.json") if errors.Is(err, os.ErrNotExist) { diff --git a/model/responses.go b/model/responses.go index fa7961c..da80be8 100644 --- a/model/responses.go +++ b/model/responses.go @@ -2,29 +2,39 @@ // between the computations of the bot and the driver code, in bot.go package model +import tgbotapi "github.com/musianisamuele/telegram-bot-api" + // CommandResponse is returned by the command handler, it contains information // about the command computation. type CommandResponse struct { Text string NextCommand string + Rows tgbotapi.InlineKeyboardMarkup } // makeResponse creates a CommandResponse with the given text and nextCommand -func makeResponse(text string, nextCommand string) CommandResponse { +func makeResponse(text string, nextCommand string, rows tgbotapi.InlineKeyboardMarkup) CommandResponse { return CommandResponse{ Text: text, NextCommand: nextCommand, + Rows: rows, } } // makeResponseWithText creates a CommandResponse with the given text (and no nextCommand) func makeResponseWithText(text string) CommandResponse { - return makeResponse(text, "") + return makeResponse(text, "", tgbotapi.InlineKeyboardMarkup{}) } // makeResponseWithNextCommand creates a CommandResponse with the given nextCommand (and no text) func makeResponseWithNextCommand(nextCommand string) CommandResponse { - return makeResponse("", nextCommand) + return makeResponse("", nextCommand, tgbotapi.InlineKeyboardMarkup{}) +} + +// makeResponseWithInlineKeyboard creates a CommandResponse with the given array +// of elements for the keyboard array. +func makeResponseWithInlineKeyboard(rows tgbotapi.InlineKeyboardMarkup) CommandResponse { + return makeResponse("", "", rows) } // IsEmpty returns true if the CommandResponse has no text and no nextCommand @@ -41,3 +51,8 @@ func (r CommandResponse) HasText() bool { func (r CommandResponse) HasNextCommand() bool { return r.NextCommand != "" } + +// HasButtonRows returns true if the CommandResponse has some rows +func (r CommandResponse) HasRows() bool { + return len(r.Rows.InlineKeyboard) > 0 +} diff --git a/model/responses_test.go b/model/responses_test.go index 67a0294..d5ba23b 100644 --- a/model/responses_test.go +++ b/model/responses_test.go @@ -3,6 +3,8 @@ package model import ( "reflect" "testing" + + tgbotapi "github.com/musianisamuele/telegram-bot-api" ) func TestCommandResponse_IsEmpty(t *testing.T) { @@ -160,7 +162,7 @@ func Test_makeResponse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := makeResponse(tt.args.text, tt.args.nextCommand); !reflect.DeepEqual(got, tt.want) { + if got := makeResponse(tt.args.text, tt.args.nextCommand, tgbotapi.InlineKeyboardMarkup{}); !reflect.DeepEqual(got, tt.want) { t.Errorf("makeResponse() = %v, want %v", got, tt.want) } }) diff --git a/model/timetables.go b/model/timetables.go new file mode 100644 index 0000000..ed980b5 --- /dev/null +++ b/model/timetables.go @@ -0,0 +1,67 @@ +package model + +import ( + "fmt" + "strings" + "time" + + tgbotapi "github.com/musianisamuele/telegram-bot-api" +) + +type InlineKeyboardRows [][]tgbotapi.InlineKeyboardButton + +// Returns a group of button rows for a selected groups on `timetables` +func GetTimetableCoursesRows(timetables *map[string]Timetable) InlineKeyboardRows { + rows := make([][]tgbotapi.InlineKeyboardButton, len(*timetables)) + + i := 0 + for callback, timetable := range *timetables { + row := tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(timetable.Course, fmt.Sprintf("lectures_%s", callback))) + rows[i] = row + i++ + } + + return rows +} + +// Returns buttons which permits to choose the day for the timetable +func ChooseTimetableDay(callback_text string) InlineKeyboardRows { + rows := make([][]tgbotapi.InlineKeyboardButton, 7) + var weekdays = [7]string{ + "Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", + } + var months = [12]string{ + "Dicembre", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", + } + + dt := time.Now() + + for day := 0; day < 7; day++ { + rows[day] = tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%s %d %s", weekdays[dt.Weekday()%7], dt.Day(), months[dt.Month()%12]), fmt.Sprintf("%s_day_%d", callback_text, dt.Unix()))) + dt = dt.AddDate(0, 0, 1) + } + + return rows +} + +// Returns a group of buttons rows for the available years of a `course` +func GetLectureYears(callback_text string, course string) InlineKeyboardRows { + yearsNro := 3 + // Master degrees has a duration of only 2 years + if strings.HasPrefix(callback_text, "lectures_lm") { + yearsNro = 2 + } + rows := make([][]tgbotapi.InlineKeyboardButton, yearsNro) + + i := 1 + for i <= yearsNro { + buttonText := fmt.Sprintf("%s: %d\u00b0 anno", course, i) + buttonCallback := fmt.Sprintf("%s_y_%d", callback_text, i) + row := tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(buttonText, buttonCallback)) + rows[i-1] = row + + i++ + } + + return rows +} diff --git a/model/timetables_test.go b/model/timetables_test.go new file mode 100644 index 0000000..d1e10c0 --- /dev/null +++ b/model/timetables_test.go @@ -0,0 +1,157 @@ +package model + +import ( + "fmt" + "testing" + "time" + + tgbotapi "github.com/musianisamuele/telegram-bot-api" +) + +func TestGetTimetableCoursesRows(t *testing.T) { + timetables := make([]map[string]Timetable, 2) + + timetables[0] = map[string]Timetable{ + "l_informatica": { + Course: "Informatica", + Type: "laurea", + Name: "informatica", + }, + } + timetables[1] = map[string]Timetable{ + "lm_informatica_software_techniques": { + Course: "Informatica Magistrale - Tecniche del software", + Type: "magistrale", + Name: "informatica", + Curriculum: "A58-000", + }, + } + wants := make([]InlineKeyboardRows, 2) + wants[0] = InlineKeyboardRows{tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica", "lectures_l_informatica"))} + wants[1] = InlineKeyboardRows{tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica Magistrale - Tecniche del software", "lectures_lm_informatica_software_techniques"))} + + type args struct { + data []map[string]Timetable + } + tests := []struct { + name string + args args + want []InlineKeyboardRows + }{ + { + name: "All the timetables from the map", + args: args{data: timetables}, + want: wants, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for idx, timetable := range tt.args.data { + var got InlineKeyboardRows = GetTimetableCoursesRows(&timetable) + if len(got) != len(tt.want[idx]) { + t.Errorf("GetTimetableCoursesRows() = %v, want %v", got, tt.want) + } else { + for i, v := range got { + for j, w := range v { + if w.Text != tt.want[idx][i][j].Text || *w.CallbackData != *tt.want[idx][i][j].CallbackData { + t.Errorf("GetTimetableCoursesRows() = %v, want %v", w, tt.want[idx][i][j]) + } + } + } + } + } + }) + } + +} + +func TestChooseTimetableDay(t *testing.T) { + dt := time.Now() + var weekdays = [7]string{ + "Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", + } + var months = [12]string{ + "Dicembre", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", + } + + type args struct { + data string + } + tests := []struct { + name string + args args + want InlineKeyboardRows + }{ + { + name: "Get lectures for the week", + args: args{data: "lectures_lm_informatica_software_techniques"}, + want: make([][]tgbotapi.InlineKeyboardButton, 7), + }, + } + + for day := 0; day < 7; day++ { + tests[0].want[day] = tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%s %d %s", weekdays[dt.Weekday()%7], dt.Day(), months[dt.Month()%12]), fmt.Sprintf("%s_day_%d", "lectures_lm_informatica_software_techniques", dt.Unix()))) + dt = dt.AddDate(0, 0, 1) + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got InlineKeyboardRows = ChooseTimetableDay(tt.args.data) + if len(got) != len(tt.want) { + t.Errorf("ChooseTimetableDay() = %v, want %v", got, tt.want) + } else { + for i, v := range got { + for j, w := range v { + if w.Text != tt.want[i][j].Text || *w.CallbackData != *tt.want[i][j].CallbackData { + t.Errorf("ChooseTimetableDay() = %v, want %v", w, tt.want[i][j]) + } + } + } + } + }) + } +} + +func TestGetLectureYears(t *testing.T) { + type args struct { + data [2]string + } + tests := []struct { + name string + args args + want InlineKeyboardRows + }{ + { + name: "Get rows for bachelor's degree", + args: args{data: [2]string{"lectures_l_informatica", "Informatica"}}, + want: InlineKeyboardRows{ + tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica: 1\u00b0 anno", "lectures_l_informatica_y_1")), + tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica: 2\u00b0 anno", "lectures_l_informatica_y_2")), + tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica: 3\u00b0 anno", "lectures_l_informatica_y_3")), + }, + }, + { + name: "Get rows for master's degree", + args: args{data: [2]string{"lectures_lm_informatica_software_techniques", "Informatica Magistrale"}}, + want: InlineKeyboardRows{ + tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica Magistrale: 1\u00b0 anno", "lectures_lm_informatica_software_techniques_y_1")), + tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Informatica Magistrale: 2\u00b0 anno", "lectures_lm_informatica_software_techniques_y_2")), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got InlineKeyboardRows = GetLectureYears(tt.args.data[0], tt.args.data[1]) + if len(got) != len(tt.want) { + t.Errorf("GetLectureYears() = %v, want %v", got, tt.want) + } else { + for i, v := range got { + for j, w := range v { + if w.Text != tt.want[i][j].Text || *w.CallbackData != *tt.want[i][j].CallbackData { + t.Errorf("GetLectureYears() = %v, want %v", w, tt.want[i][j]) + } + } + } + } + }) + } +}