mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
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 <athurg@gooth.org>
This commit is contained in:
21
plugin/telegram/api_answer_callback_query.go
Normal file
21
plugin/telegram/api_answer_callback_query.go
Normal file
@@ -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
|
||||||
|
}
|
@@ -2,18 +2,32 @@ package telegram
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EditMessage make an editMessageText api request.
|
// 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{
|
formData := url.Values{
|
||||||
"message_id": {strconv.Itoa(messageID)},
|
"message_id": {strconv.Itoa(messageID)},
|
||||||
"chat_id": {strconv.Itoa(chatID)},
|
"chat_id": {strconv.Itoa(chatID)},
|
||||||
"text": {text},
|
"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
|
var result Message
|
||||||
err := b.postForm(ctx, "/editMessageText", formData, &result)
|
err := b.postForm(ctx, "/editMessageText", formData, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -13,7 +13,8 @@ import (
|
|||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
BotToken(ctx context.Context) string
|
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 {
|
type Bot struct {
|
||||||
@@ -44,37 +45,51 @@ func (b *Bot) Start(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
singleMessages := make([]Message, 0, len(updates))
|
||||||
groupMessages := make([]Message, 0, len(updates))
|
groupMessages := make([]Message, 0, len(updates))
|
||||||
|
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
offset = update.UpdateID + 1
|
offset = update.UpdateID + 1
|
||||||
if update.Message == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
message := *update.Message
|
|
||||||
|
|
||||||
// skip message other than text or photo
|
// handle CallbackQuery update
|
||||||
if message.Text == nil && message.Photo == nil {
|
if update.CallbackQuery != nil {
|
||||||
_, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, "Only text or photo message be supported")
|
err := b.handler.CallbackQueryHandle(ctx, b, *update.CallbackQuery)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group message need do more
|
// handle Message update
|
||||||
if message.MediaGroupID != nil {
|
if update.Message != nil {
|
||||||
groupMessages = append(groupMessages, message)
|
message := *update.Message
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.handleSingleMessage(ctx, message)
|
// skip message other than text or photo
|
||||||
if err != nil {
|
if message.Text == nil && message.Photo == nil {
|
||||||
log.Error(fmt.Sprintf("fail to handleSingleMessage for messageID=%d", message.MessageID), zap.Error(err))
|
_, 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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = b.handleSingleMessages(ctx, singleMessages)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fail to handle singleMessage", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
err = b.handleGroupMessages(ctx, groupMessages)
|
err = b.handleGroupMessages(ctx, groupMessages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("fail to handle plain text message", zap.Error(err))
|
log.Error("fail to handle plain text message", zap.Error(err))
|
||||||
|
11
plugin/telegram/callback_query.go
Normal file
11
plugin/telegram/callback_query.go
Normal file
@@ -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"`
|
||||||
|
}
|
@@ -3,50 +3,26 @@ package telegram
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/usememos/memos/common/log"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// notice message send to telegram.
|
// handleSingleMessages handle single messages not belongs to group.
|
||||||
const (
|
func (b *Bot) handleSingleMessages(ctx context.Context, messages []Message) error {
|
||||||
workingMessage = "Working on send your memo..."
|
for _, message := range messages {
|
||||||
successMessage = "Success"
|
var blobs map[string][]byte
|
||||||
)
|
|
||||||
|
|
||||||
// handleSingleMessage handle a message not belongs to group.
|
// download blob if provided
|
||||||
func (b *Bot) handleSingleMessage(ctx context.Context, message Message) error {
|
if len(message.Photo) > 0 {
|
||||||
reply, err := b.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, workingMessage)
|
filepath, blob, err := b.downloadFileID(ctx, message.GetMaxPhotoFileID())
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fail to SendReplyMessage: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
if err != nil {
|
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)
|
err := b.handler.MessageHandle(ctx, b, message, blobs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, err := b.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()); err != nil {
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -80,23 +56,12 @@ func (b *Bot) handleGroupMessages(ctx context.Context, groupMessages []Message)
|
|||||||
|
|
||||||
// Handle each group message
|
// Handle each group message
|
||||||
for groupID, message := range messages {
|
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
|
// replace Caption with all Caption in the group
|
||||||
caption := captions[groupID]
|
caption := captions[groupID]
|
||||||
message.Caption = &caption
|
message.Caption = &caption
|
||||||
if err := b.handler.MessageHandle(ctx, message, blobs[groupID]); err != nil {
|
err := b.handler.MessageHandle(ctx, b, message, blobs[groupID])
|
||||||
if _, err = b.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fail to EditMessage: %s", err)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
plugin/telegram/inline_keyboard_button.go
Normal file
6
plugin/telegram/inline_keyboard_button.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
type InlineKeyboardButton struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
CallbackData string `json:"callback_data"`
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
UpdateID int `json:"update_id"`
|
UpdateID int `json:"update_id"`
|
||||||
Message *Message `json:"message"`
|
Message *Message `json:"message"`
|
||||||
|
CallbackQuery *CallbackQuery `json:"callback_query"`
|
||||||
}
|
}
|
||||||
|
@@ -25,13 +25,24 @@ func (t *telegramHandler) BotToken(ctx context.Context) string {
|
|||||||
return t.store.GetSystemSettingValueOrDefault(&ctx, api.SystemSettingTelegramBotTokenName, "")
|
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
|
var creatorID int
|
||||||
userSettingList, err := t.store.FindUserSettingList(ctx, &api.UserSettingFind{
|
userSettingList, err := t.store.FindUserSettingList(ctx, &api.UserSettingFind{
|
||||||
Key: api.UserSettingTelegramUserIDKey,
|
Key: api.UserSettingTelegramUserIDKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
for _, userSetting := range userSettingList {
|
||||||
var value string
|
var value string
|
||||||
@@ -45,7 +56,8 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
if creatorID == 0 {
|
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
|
// create memo
|
||||||
@@ -63,11 +75,13 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me
|
|||||||
|
|
||||||
memoMessage, err := t.store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(&memoCreate))
|
memoMessage, err := t.store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(&memoCreate))
|
||||||
if err != nil {
|
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 {
|
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
|
// create resources
|
||||||
@@ -90,10 +104,12 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me
|
|||||||
}
|
}
|
||||||
resource, err := t.store.CreateResource(ctx, &resourceCreate)
|
resource, err := t.store.CreateResource(ctx, &resourceCreate)
|
||||||
if err != nil {
|
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 {
|
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{
|
_, err = t.store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||||
@@ -101,8 +117,57 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Me
|
|||||||
ResourceID: resource.ID,
|
ResourceID: resource.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
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 [][]telegram.InlineKeyboardButton{buttons}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user