package handler import ( "encoding/json" "fmt" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" "github.com/mymmrac/telego" tu "github.com/mymmrac/telego/telegoutil" "strings" ) type CallbackQueryHandler struct { iface.Base } func NewCallbackQueryHandler(app iface.App) Handler { return &CallbackQueryHandler{iface.NewBase(app, 0, 0)} } func (h *CallbackQueryHandler) Handle(wh telego.Update) error { cq := wh.CallbackQuery if cq.Message.GetChat().Type == telego.ChatTypePrivate { user, err := h.App.DB().ForUser().ByTelegramID(cq.From.ID) if err != nil { return err } if user != nil && user.ID > 0 { return h.handleSetupKey(cq, user) } return nil } var pl util.Payload if err := json.Unmarshal([]byte(cq.Data), &pl); err != nil { return err } chat, err := h.App.DB().ForChat().ByTelegramID(pl.Chat) if err != nil { return err } user, err := h.App.DB().ForUser().ByID(chat.UserID) if err != nil { return err } loc := h.Localizer(user.Language) switch pl.Action { case util.PayloadActionVote: poll, found, err := h.findPollByMessage(cq.Message, loc) if err != nil { return err } if !found { return nil } return h.handleVote(cq.From, chat, cq.Message, poll, pl, loc) case util.PayloadActionVoteFinish: poll, found, err := h.findPollByMessage(cq.Message, loc) if err != nil { return err } if !found { return nil } return h.handleVoteFinish(cq.From, chat, cq.Message, poll, pl, loc) case util.PayloadActionVoteSendRedmine: poll, found, err := h.findPollByMessage(cq.Message, loc) if err != nil { return err } if !found { return nil } return h.handleVoteSendRedmine(cq.From, chat, cq.Message, poll, pl, loc) default: } return nil } func (h *CallbackQueryHandler) handleVoteFinish(from telego.User, chat *model.Chat, msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error { dotOrColon := "." if poll.Subject != "" { dotOrColon = ": " } if len(poll.Votes) == 0 { store.Polls.Delete(msg.GetMessageID()) _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(pl.Chat), MessageID: msg.GetMessageID(), Text: fmt.Sprintf("%s\n%s\n%s", loc.Template("poker_start", map[string]interface{}{ "Name": poll.Initiator, "DotOrColon": dotOrColon, "Subject": poll.Subject, }), loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}), loc.Message("poker_ended_no_results"), ), ParseMode: telego.ModeMarkdown, }) h.App.Log().Debugf("finished poll: %#v", poll) return err } poll.Calculate() store.Polls.Set(msg.GetMessageID(), poll) _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(pl.Chat), MessageID: msg.GetMessageID(), Text: fmt.Sprintf("%s\n%s\n\n%s\n%s\n\n%s", loc.Template("poker_start", map[string]interface{}{ "Name": poll.Initiator, "DotOrColon": dotOrColon, "Subject": poll.Subject, }), loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}), loc.Message("voted"), h.voters(poll.Votes, loc), loc.Template("poker_results", poll.Result), ), ParseMode: telego.ModeMarkdown, ReplyMarkup: h.getPollAfterEndKeyboard(chat, poll, loc), }) h.App.Log().Debugf("finished poll: %#v", poll) return err } func (h *CallbackQueryHandler) handleVoteSendRedmine(from telego.User, chat *model.Chat, msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error { dotOrColon := "." if poll.Subject != "" { dotOrColon = ": " } updateMessage := func(tag string) error { _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(pl.Chat), MessageID: msg.GetMessageID(), Text: fmt.Sprintf("%s\n%s\n\n%s\n%s\n\n%s\n\n%s", loc.Template("poker_start", map[string]interface{}{ "Name": poll.Initiator, "DotOrColon": dotOrColon, "Subject": poll.Subject, }), loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}), loc.Message("voted"), h.voters(poll.Votes, loc), loc.Template("poker_results", poll.Result), loc.Message(tag), ), ParseMode: telego.ModeMarkdown, }) return err } h.App.Log().Debugf("received vote send for poll: %#v", poll) if !poll.CanRedmine || poll.TaskID == 0 { store.Polls.Delete(msg.GetMessageID()) h.App.Log().Errorf("cannot send info to Redmine for %d: CanRedmine: %t", poll.TaskID, poll.CanRedmine) return updateMessage("poker_redmine_cannot_send") } integrationData, err := h.App.DB().ForIntegration().LoadForChatAndType(chat.ID, model.RedmineIntegration) if err != nil { store.Polls.Delete(msg.GetMessageID()) h.App.Log().Errorf("cannot send info to Redmine for %d: %s", poll.TaskID, err) return updateMessage("poker_redmine_cannot_send") } err = integration.New(*integrationData, h.App.Log()).PushVoteResult(poll.TaskID, poll.Result.RoundHalf) if err != nil { store.Polls.Delete(msg.GetMessageID()) h.App.Log().Errorf("cannot send info to Redmine for %d: %s", poll.TaskID, err) return updateMessage("poker_redmine_cannot_send") } return updateMessage("poker_redmine_sent") } func (h *CallbackQueryHandler) handleVote(from telego.User, chat *model.Chat, msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error { var found bool for i, storedVote := range poll.Votes { if storedVote.ID == from.ID { storedVote.Name = h.pollMemberName(&from) storedVote.Vote = pl.Vote().Vote poll.Votes[i] = storedVote found = true break } } if !found { poll.Votes = append(poll.Votes, store.MemberVote{ Member: util.Member{ ID: from.ID, Name: h.pollMemberName(&from), }, Vote: pl.Vote().Vote, }) } store.Polls.Set(msg.GetMessageID(), poll) h.App.Log().Debugf("processed vote %#v for poll: %#v", pl.Vote(), poll) dotOrColon := "." if poll.Subject != "" { dotOrColon = ": " } _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(pl.Chat), MessageID: msg.GetMessageID(), Text: fmt.Sprintf("%s\n%s\n%s", loc.Template("poker_start", map[string]interface{}{ "Name": poll.Initiator, "DotOrColon": dotOrColon, "Subject": poll.Subject, }), loc.Message("voted"), h.votersWithoutVotes(poll.Votes, loc), ), ReplyMarkup: h.getPollKeyboard(chat, loc), ParseMode: telego.ModeMarkdown, }) return err } func (h *CallbackQueryHandler) pollMemberName(user *telego.User) string { return fmt.Sprintf("%s (@%s)", strings.TrimRight(user.FirstName+" "+user.LastName, " "), user.Username) } func (h *CallbackQueryHandler) voters(votes []store.MemberVote, loc locale.Localizer) string { var sb strings.Builder sb.Grow(20 * len(votes)) for _, vote := range votes { sb.WriteString(loc.Template("vote_result", map[string]interface{}{ "Name": vote.Name, "Vote": vote.Vote, }) + "\n") } return strings.TrimRight(sb.String(), "\n") } func (h *CallbackQueryHandler) votersWithoutVotes(votes []store.MemberVote, loc locale.Localizer) string { var sb strings.Builder sb.Grow(20 * len(votes)) for _, vote := range votes { sb.WriteString(loc.Template("voter", map[string]interface{}{"Name": vote.Name}) + "\n") } return strings.TrimRight(sb.String(), "\n") } func (h *CallbackQueryHandler) getPollKeyboard(chat *model.Chat, loc locale.Localizer) *telego.InlineKeyboardMarkup { switch chat.KeyboardType { case model.StandardKeyboard: return util.StandardVoteKeyboard(chat.TelegramID, loc) case model.StoryPointsKeyboard: return util.StoryPointsVoteKeyboard(chat.TelegramID, loc) } return nil } func (h *CallbackQueryHandler) getPollAfterEndKeyboard(chat *model.Chat, poll store.PollState, loc locale.Localizer) *telego.InlineKeyboardMarkup { rm, err := h.App.DB().ForIntegration().LoadForChatAndType(chat.ID, model.RedmineIntegration) if err != nil { return nil } if rm != nil && rm.ID > 0 && poll.CanRedmine && poll.TaskID > 0 && len(poll.Votes) > 1 { return util.SendRedmineKeyboard(chat.TelegramID, poll.Result.RoundHalf, loc) } return nil } func (h *CallbackQueryHandler) findPollByMessage(msg telego.MaybeInaccessibleMessage, loc locale.Localizer) (store.PollState, bool, error) { poll, found := store.Polls.Get(msg.GetMessageID()) if !found { _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(msg.GetChat().ID), MessageID: msg.GetMessageID(), Text: loc.Message("cannot_find_poll"), }) return store.PollState{}, false, err } return poll, true, nil } func (h *CallbackQueryHandler) handleSetupKey(cq *telego.CallbackQuery, user *model.User) error { var pl util.Payload if err := json.Unmarshal([]byte(cq.Data), &pl); err != nil { return err } switch pl.Action { case util.PayloadActionChooseKeyboard: return h.handleChooseKeyboard(pl, cq.Message.GetMessageID(), user) case util.PayloadActionYesNo: answer := pl.YesNoAnswer() switch answer.Type { case util.QuestionTypeRedmine: return h.handleAnswerRedmine(answer, pl.Chat, cq.Message.GetMessageID(), user) case util.QuestionTypeRedmineHours: return h.handleAnswerRedmineHours(answer, pl.Chat, cq.Message.GetMessageID(), user) case util.QuestionTypeRedmineSendResult: return h.handleAnswerRedmineSendResults(answer, cq.Message.GetMessageID(), user) } default: return nil } return nil } func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int, user *model.User) error { cr := h.App.DB().ForChat() result := pl.KeyboardChoice() if pl.User != user.TelegramID { return nil } chat, err := cr.ByTelegramID(pl.Chat) if err != nil { return err } if chat == nil || chat.ID == 0 { return nil } chat.KeyboardType = model.KeyboardType(result.Type) if err := cr.Save(chat); err != nil { return err } kbTypeName := "standard_vote_keyboard" if model.KeyboardType(result.Type) == model.StoryPointsKeyboard { kbTypeName = "sp_vote_keyboard" } loc := h.Localizer(user.Language) _, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Template("chosen_keyboard", map[string]interface{}{"Name": loc.Message(kbTypeName)}), }) _, err = h.App.TG().SendMessage(&telego.SendMessageParams{ ChatID: tu.ID(user.ChatID), Text: loc.Message("ask_for_redmine"), ParseMode: telego.ModeMarkdown, ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{ { { Text: loc.Message("yes"), CallbackData: util.NewRedmineQuestionPayload(user.TelegramID, chat.TelegramID, true).String(), }, { Text: loc.Message("no"), CallbackData: util.NewRedmineQuestionPayload(user.TelegramID, chat.TelegramID, false).String(), }, }, }}, }) return err } func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID int64, msgID int, user *model.User) error { loc := h.Localizer(user.Language) if !answer.Result { _, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Message("redmine_will_not_be_configured"), }) return util.SendSetupDone(h.App.TG(), h.App.TGProfile().Username, user.ChatID, loc) } _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Message("please_send_redmine_url"), }) store.RedmineSetups.Set(user.ChatID, &store.RedmineSetup{Chat: chatID}) return err } func (h *CallbackQueryHandler) handleAnswerRedmineHours(answer util.Answer, chatID int64, msgID int, user *model.User) error { loc := h.Localizer(user.Language) message := "redmine_hours_will_not_be_configured" if answer.Result { message = "redmine_hours_will_be_configured" } _, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Message(message), }) rs, found := store.RedmineSetups.Get(user.ChatID) if found { rs.SaveHours = answer.Result if err := h.updateRedmineIntegration(rs, loc, user); err != nil { return err } } _, err := h.App.TG().SendMessage(&telego.SendMessageParams{ ChatID: tu.ID(user.ChatID), Text: loc.Message("should_send_poker_to_redmine"), ParseMode: telego.ModeMarkdown, ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{ { { Text: loc.Message("yes"), CallbackData: util.NewRedmineSendResultQuestionPayload(user.TelegramID, chatID, true).String(), }, { Text: loc.Message("no"), CallbackData: util.NewRedmineSendResultQuestionPayload(user.TelegramID, chatID, false).String(), }, }, }}, }) return err } func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer, msgID int, user *model.User) error { loc := h.Localizer(user.Language) if !answer.Result { _, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Message("redmine_poker_will_not_be_configured"), }) return util.SendSetupDone(h.App.TG(), h.App.TGProfile().Username, user.ChatID, loc) } _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), MessageID: msgID, Text: loc.Message("specify_result_field"), }) rs, _ := store.RedmineSetups.Get(user.ChatID) rs.WaitingForSPField = true return err } func (h *CallbackQueryHandler) updateRedmineIntegration(rs *store.RedmineSetup, loc locale.Localizer, user *model.User) error { dbChat, err := h.App.DB().ForChat().ByTelegramID(rs.Chat) if dbChat == nil || dbChat.ID == 0 { _ = util.SendInternalError(h.App.TG(), user.ChatID, loc) return err } savedRedmine, err := h.App.DB().ForIntegration().LoadForChatAndType(dbChat.ID, model.RedmineIntegration) if savedRedmine == nil || savedRedmine.ID == 0 { _ = util.SendInternalError(h.App.TG(), user.ChatID, loc) return err } var shouldUpdate bool savedRS := savedRedmine.LoadRedmine() if savedRS.SaveHours != rs.SaveHours { savedRS.SaveHours = rs.SaveHours shouldUpdate = true } if savedRS.SPFieldName != rs.SPFieldName { savedRS.SPFieldName = rs.SPFieldName shouldUpdate = true } if shouldUpdate { savedRedmine.StoreRedmine(savedRS) return h.App.DB().ForIntegration().Save(savedRedmine) } return nil }