From 4d59689126ec65698ab068a541ab336d6dd7dc6d Mon Sep 17 00:00:00 2001 From: Athurg Gooth Date: Wed, 14 Jun 2023 22:10:01 +0800 Subject: [PATCH] feat: set memo visibility in telegram (#1824) * Add telegram.Bot in MessageHandler * Change single message handler like group messages * Move message notify wrapper from plugin to server * Add keyboard buttons on Telegram reply message * Add support to telegram CallbackQuery update * Set visibility in callbackQuery * Change original reply message after callbackQuery --------- Co-authored-by: Athurg Feng --- plugin/telegram/api_answer_callback_query.go | 21 +++++ plugin/telegram/api_edit_message.go | 16 +++- plugin/telegram/bot.go | 49 ++++++++---- plugin/telegram/callback_query.go | 11 +++ plugin/telegram/handle.go | 65 ++++----------- plugin/telegram/inline_keyboard_button.go | 6 ++ plugin/telegram/update.go | 5 +- server/telegram.go | 83 +++++++++++++++++--- 8 files changed, 177 insertions(+), 79 deletions(-) create mode 100644 plugin/telegram/api_answer_callback_query.go create mode 100644 plugin/telegram/callback_query.go create mode 100644 plugin/telegram/inline_keyboard_button.go diff --git a/plugin/telegram/api_answer_callback_query.go b/plugin/telegram/api_answer_callback_query.go new file mode 100644 index 00000000..3507f47e --- /dev/null +++ b/plugin/telegram/api_answer_callback_query.go @@ -0,0 +1,21 @@ +package telegram + +import ( + "context" + "net/url" +) + +// AnswerCallbackQuery make an answerCallbackQuery api request. +func (b *Bot) AnswerCallbackQuery(ctx context.Context, callbackQueryID, text string) error { + formData := url.Values{ + "callback_query_id": {callbackQueryID}, + "text": {text}, + } + + err := b.postForm(ctx, "/answerCallbackQuery", formData, nil) + if err != nil { + return err + } + + return nil +} diff --git a/plugin/telegram/api_edit_message.go b/plugin/telegram/api_edit_message.go index 67accad7..798518b6 100644 --- a/plugin/telegram/api_edit_message.go +++ b/plugin/telegram/api_edit_message.go @@ -2,18 +2,32 @@ package telegram import ( "context" + "encoding/json" + "fmt" "net/url" "strconv" ) // EditMessage make an editMessageText api request. -func (b *Bot) EditMessage(ctx context.Context, chatID, messageID int, text string) (*Message, error) { +func (b *Bot) EditMessage(ctx context.Context, chatID, messageID int, text string, inlineKeyboards [][]InlineKeyboardButton) (*Message, error) { formData := url.Values{ "message_id": {strconv.Itoa(messageID)}, "chat_id": {strconv.Itoa(chatID)}, "text": {text}, } + if len(inlineKeyboards) > 0 { + var markup struct { + InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + } + markup.InlineKeyboard = inlineKeyboards + data, err := json.Marshal(markup) + if err != nil { + return nil, fmt.Errorf("fail to encode inlineKeyboard: %s", err) + } + formData.Set("reply_markup", string(data)) + } + var result Message err := b.postForm(ctx, "/editMessageText", formData, &result) if err != nil { diff --git a/plugin/telegram/bot.go b/plugin/telegram/bot.go index b8480fbc..54345717 100644 --- a/plugin/telegram/bot.go +++ b/plugin/telegram/bot.go @@ -13,7 +13,8 @@ import ( type Handler interface { BotToken(ctx context.Context) string - MessageHandle(ctx context.Context, message Message, blobs map[string][]byte) error + MessageHandle(ctx context.Context, bot *Bot, message Message, blobs map[string][]byte) error + CallbackQueryHandle(ctx context.Context, bot *Bot, callbackQuery CallbackQuery) error } type Bot struct { @@ -44,37 +45,51 @@ func (b *Bot) Start(ctx context.Context) { continue } + singleMessages := make([]Message, 0, len(updates)) groupMessages := make([]Message, 0, len(updates)) for _, update := range updates { offset = update.UpdateID + 1 - if update.Message == nil { - continue - } - message := *update.Message - // skip message other than text or photo - if message.Text == nil && message.Photo == nil { - _, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, "Only text or photo message be supported") + // handle CallbackQuery update + if update.CallbackQuery != nil { + err := b.handler.CallbackQueryHandle(ctx, b, *update.CallbackQuery) if err != nil { - log.Error(fmt.Sprintf("fail to telegram.SendReplyMessage for messageID=%d", message.MessageID), zap.Error(err)) + log.Error("fail to handle CallbackQuery", zap.Error(err)) } - continue - } - // Group message need do more - if message.MediaGroupID != nil { - groupMessages = append(groupMessages, message) continue } - err = b.handleSingleMessage(ctx, message) - if err != nil { - log.Error(fmt.Sprintf("fail to handleSingleMessage for messageID=%d", message.MessageID), zap.Error(err)) + // handle Message update + if update.Message != nil { + message := *update.Message + + // skip message other than text or photo + if message.Text == nil && message.Photo == nil { + _, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, "Only text or photo message be supported") + if err != nil { + log.Error(fmt.Sprintf("fail to telegram.SendReplyMessage for messageID=%d", message.MessageID), zap.Error(err)) + } + continue + } + + // Group message need do more + if message.MediaGroupID != nil { + groupMessages = append(groupMessages, message) + continue + } + + singleMessages = append(singleMessages, message) continue } } + err = b.handleSingleMessages(ctx, singleMessages) + if err != nil { + log.Error("fail to handle singleMessage", zap.Error(err)) + } + err = b.handleGroupMessages(ctx, groupMessages) if err != nil { log.Error("fail to handle plain text message", zap.Error(err)) diff --git a/plugin/telegram/callback_query.go b/plugin/telegram/callback_query.go new file mode 100644 index 00000000..a25ec3f7 --- /dev/null +++ b/plugin/telegram/callback_query.go @@ -0,0 +1,11 @@ +package telegram + +type CallbackQuery struct { + ID string `json:"id"` + From User `json:"from"` + Message *Message `json:"message"` + InlineMessageID string `json:"inline_message_id"` + ChatInstance string `json:"chat_instance"` + Data string `json:"data"` + GameShortName string `json:"game_short_name"` +} diff --git a/plugin/telegram/handle.go b/plugin/telegram/handle.go index e361cd8f..f504955d 100644 --- a/plugin/telegram/handle.go +++ b/plugin/telegram/handle.go @@ -3,50 +3,26 @@ package telegram import ( "context" "fmt" - - "github.com/usememos/memos/common/log" - "go.uber.org/zap" -) - -// notice message send to telegram. -const ( - workingMessage = "Working on send your memo..." - successMessage = "Success" ) -// handleSingleMessage handle a message not belongs to group. -func (b *Bot) handleSingleMessage(ctx context.Context, message Message) error { - reply, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, workingMessage) - if err != nil { - return fmt.Errorf("fail to SendReplyMessage: %s", err) - } - - var blobs map[string][]byte +// handleSingleMessages handle single messages not belongs to group. +func (b *Bot) handleSingleMessages(ctx context.Context, messages []Message) error { + for _, message := range messages { + var blobs map[string][]byte - // download blob if need - if len(message.Photo) > 0 { - filepath, blob, err := b.downloadFileID(ctx, message.GetMaxPhotoFileID()) - if err != nil { - log.Error("fail to downloadFileID", zap.Error(err)) - _, err = b.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()) + // download blob if provided + if len(message.Photo) > 0 { + filepath, blob, err := b.downloadFileID(ctx, message.GetMaxPhotoFileID()) if err != nil { - return fmt.Errorf("fail to EditMessage: %s", err) + return err } - return fmt.Errorf("fail to downloadFileID: %s", err) + blobs = map[string][]byte{filepath: blob} } - blobs = map[string][]byte{filepath: blob} - } - err = b.handler.MessageHandle(ctx, message, blobs) - if err != nil { - if _, err := b.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()); err != nil { - return fmt.Errorf("fail to EditMessage: %s", err) + err := b.handler.MessageHandle(ctx, b, message, blobs) + if err != nil { + return err } - return fmt.Errorf("fail to MessageHandle: %s", err) - } - - if _, err := b.EditMessage(ctx, message.Chat.ID, reply.MessageID, successMessage); err != nil { - return fmt.Errorf("fail to EditMessage: %s", err) } return nil @@ -80,23 +56,12 @@ func (b *Bot) handleGroupMessages(ctx context.Context, groupMessages []Message) // Handle each group message for groupID, message := range messages { - reply, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, workingMessage) - if err != nil { - return fmt.Errorf("fail to SendReplyMessage: %s", err) - } - // replace Caption with all Caption in the group caption := captions[groupID] message.Caption = &caption - if err := b.handler.MessageHandle(ctx, message, blobs[groupID]); err != nil { - if _, err = b.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()); err != nil { - return fmt.Errorf("fail to EditMessage: %s", err) - } - return fmt.Errorf("fail to MessageHandle: %s", err) - } - - if _, err := b.EditMessage(ctx, message.Chat.ID, reply.MessageID, successMessage); err != nil { - return fmt.Errorf("fail to EditMessage: %s", err) + err := b.handler.MessageHandle(ctx, b, message, blobs[groupID]) + if err != nil { + return err } } diff --git a/plugin/telegram/inline_keyboard_button.go b/plugin/telegram/inline_keyboard_button.go new file mode 100644 index 00000000..4883c9bb --- /dev/null +++ b/plugin/telegram/inline_keyboard_button.go @@ -0,0 +1,6 @@ +package telegram + +type InlineKeyboardButton struct { + Text string `json:"text"` + CallbackData string `json:"callback_data"` +} diff --git a/plugin/telegram/update.go b/plugin/telegram/update.go index e44a3d4b..87cea9a9 100644 --- a/plugin/telegram/update.go +++ b/plugin/telegram/update.go @@ -1,6 +1,7 @@ package telegram type Update struct { - UpdateID int `json:"update_id"` - Message *Message `json:"message"` + UpdateID int `json:"update_id"` + Message *Message `json:"message"` + CallbackQuery *CallbackQuery `json:"callback_query"` } diff --git a/server/telegram.go b/server/telegram.go index 2f9d25f7..13268151 100644 --- a/server/telegram.go +++ b/server/telegram.go @@ -25,13 +25,24 @@ func (t *telegramHandler) BotToken(ctx context.Context) string { return t.store.GetSystemSettingValueOrDefault(&ctx, api.SystemSettingTelegramBotTokenName, "") } -func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Message, blobs map[string][]byte) error { +const ( + workingMessage = "Working on send your memo..." + successMessage = "Success" +) + +func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot, message telegram.Message, blobs map[string][]byte) error { + reply, err := bot.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, workingMessage) + if err != nil { + return fmt.Errorf("fail to SendReplyMessage: %s", err) + } + var creatorID int userSettingList, err := t.store.FindUserSettingList(ctx, &api.UserSettingFind{ Key: api.UserSettingTelegramUserIDKey, }) if err != nil { - return fmt.Errorf("Fail to find memo user: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Fail to find memo user: %s", err), nil) + return err } for _, userSetting := range userSettingList { var value string @@ -45,7 +56,8 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me } if creatorID == 0 { - return fmt.Errorf("Please set your telegram userid %d in UserSetting of Memos", message.From.ID) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Please set your telegram userid %d in UserSetting of Memos", message.From.ID), nil) + return err } // create memo @@ -63,11 +75,13 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me memoMessage, err := t.store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(&memoCreate)) if err != nil { - return fmt.Errorf("failed to CreateMemo: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to CreateMemo: %s", err), nil) + return err } if err := createMemoCreateActivity(ctx, t.store, memoMessage); err != nil { - return fmt.Errorf("failed to createMemoCreateActivity: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to createMemoCreateActivity: %s", err), nil) + return err } // create resources @@ -90,10 +104,12 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me } resource, err := t.store.CreateResource(ctx, &resourceCreate) if err != nil { - return fmt.Errorf("failed to CreateResource: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to CreateResource: %s", err), nil) + return err } if err := createResourceCreateActivity(ctx, t.store, resource); err != nil { - return fmt.Errorf("failed to createResourceCreateActivity: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to createResourceCreateActivity: %s", err), nil) + return err } _, err = t.store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{ @@ -101,8 +117,57 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me ResourceID: resource.ID, }) if err != nil { - return fmt.Errorf("failed to UpsertMemoResource: %s", err) + _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to UpsertMemoResource: %s", err), nil) + return err + } + } + + keyboard := generateKeyboardForMemoID(memoMessage.ID) + _, err = bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Saved as %s Memo %d", memoMessage.Visibility, memoMessage.ID), keyboard) + return err +} + +func (t *telegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram.Bot, callbackQuery telegram.CallbackQuery) error { + var memoID int + var visibility store.Visibility + n, err := fmt.Sscanf(callbackQuery.Data, "%s %d", &visibility, &memoID) + if err != nil || n != 2 { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("fail to parse callbackQuery.Data %s", callbackQuery.Data)) + } + + update := store.UpdateMemoMessage{ + ID: memoID, + Visibility: &visibility, + } + err = t.store.UpdateMemo(ctx, &update) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("fail to call UpdateMemo %s", err)) + } + + keyboard := generateKeyboardForMemoID(memoID) + _, err = bot.EditMessage(ctx, callbackQuery.Message.Chat.ID, callbackQuery.Message.MessageID, fmt.Sprintf("Saved as %s Memo %d", visibility, memoID), keyboard) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("fail to EditMessage %s", err)) + } + + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Success change Memo %d to %s", memoID, visibility)) +} + +func generateKeyboardForMemoID(id int) [][]telegram.InlineKeyboardButton { + allVisibility := []store.Visibility{ + store.Public, + store.Protected, + store.Private, + } + + buttons := make([]telegram.InlineKeyboardButton, 0, len(allVisibility)) + for _, v := range allVisibility { + button := telegram.InlineKeyboardButton{ + Text: v.String(), + CallbackData: fmt.Sprintf("%s %d", v, id), } + buttons = append(buttons, button) } - return nil + + return [][]telegram.InlineKeyboardButton{buttons} }