From 1282fe732e42f3f6a53c74e669f3f8feb84ec1c3 Mon Sep 17 00:00:00 2001 From: Athurg Gooth Date: Fri, 26 May 2023 09:43:51 +0800 Subject: [PATCH] feat: implement telegram bot plugin (#1740) --- api/system_setting.go | 4 ++ api/user_setting.go | 15 +++- plugin/telegram/api_edit_message.go | 24 +++++++ plugin/telegram/api_get_file.go | 21 ++++++ plugin/telegram/api_get_updates.go | 23 ++++++ plugin/telegram/api_send_message.go | 24 +++++++ plugin/telegram/chat.go | 9 +++ plugin/telegram/download.go | 44 ++++++++++++ plugin/telegram/file.go | 8 +++ plugin/telegram/handle.go | 104 ++++++++++++++++++++++++++++ plugin/telegram/message.go | 24 +++++++ plugin/telegram/photosize.go | 9 +++ plugin/telegram/request.go | 52 ++++++++++++++ plugin/telegram/robot.go | 79 +++++++++++++++++++++ plugin/telegram/update.go | 6 ++ plugin/telegram/user.go | 5 ++ server/memo.go | 9 ++- server/resource.go | 11 +-- server/server.go | 9 +++ server/system.go | 2 +- server/telegram.go | 102 +++++++++++++++++++++++++++ server/user.go | 4 +- store/user_setting.go | 4 +- 23 files changed, 577 insertions(+), 15 deletions(-) create mode 100644 plugin/telegram/api_edit_message.go create mode 100644 plugin/telegram/api_get_file.go create mode 100644 plugin/telegram/api_get_updates.go create mode 100644 plugin/telegram/api_send_message.go create mode 100644 plugin/telegram/chat.go create mode 100644 plugin/telegram/download.go create mode 100644 plugin/telegram/file.go create mode 100644 plugin/telegram/handle.go create mode 100644 plugin/telegram/message.go create mode 100644 plugin/telegram/photosize.go create mode 100644 plugin/telegram/request.go create mode 100644 plugin/telegram/robot.go create mode 100644 plugin/telegram/update.go create mode 100644 plugin/telegram/user.go create mode 100644 server/telegram.go diff --git a/api/system_setting.go b/api/system_setting.go index 75bdc35d..3cdc9867 100644 --- a/api/system_setting.go +++ b/api/system_setting.go @@ -34,6 +34,8 @@ const ( SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path" // SystemSettingOpenAIConfigName is the name of OpenAI config. SystemSettingOpenAIConfigName SystemSettingName = "openai-config" + // SystemSettingTelegramRobotToken is the name of Telegram Robot Token. + SystemSettingTelegramRobotTokenName SystemSettingName = "telegram-robot-token" ) // CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item. @@ -83,6 +85,8 @@ func (key SystemSettingName) String() string { return "local-storage-path" case SystemSettingOpenAIConfigName: return "openai-config" + case SystemSettingTelegramRobotTokenName: + return "telegram-robot-token" } return "" } diff --git a/api/user_setting.go b/api/user_setting.go index 2ee54dc2..c5e95a21 100644 --- a/api/user_setting.go +++ b/api/user_setting.go @@ -16,6 +16,8 @@ const ( UserSettingAppearanceKey UserSettingKey = "appearance" // UserSettingMemoVisibilityKey is the key type for user preference memo default visibility. UserSettingMemoVisibilityKey UserSettingKey = "memo-visibility" + // UserSettingTelegramUserID is the key type for telegram UserID of memos user. + UserSettingTelegramUserIDKey UserSettingKey = "telegram-user-id" ) // String returns the string format of UserSettingKey type. @@ -27,6 +29,8 @@ func (key UserSettingKey) String() string { return "appearance" case UserSettingMemoVisibilityKey: return "memo-visibility" + case UserSettingTelegramUserIDKey: + return "telegram-user-id" } return "" } @@ -96,6 +100,15 @@ func (upsert UserSettingUpsert) Validate() error { if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) { return fmt.Errorf("invalid user setting memo visibility value") } + } else if upsert.Key == UserSettingTelegramUserIDKey { + telegramUserID := 0 + err := json.Unmarshal([]byte(upsert.Value), &telegramUserID) + if err != nil { + return fmt.Errorf("failed to unmarshal user setting telegram userid value") + } + if telegramUserID <= 0 { + return fmt.Errorf("invalid user setting telegram userid value") + } } else { return fmt.Errorf("invalid user setting key") } @@ -104,7 +117,7 @@ func (upsert UserSettingUpsert) Validate() error { } type UserSettingFind struct { - UserID int + UserID *int Key UserSettingKey `json:"key"` } diff --git a/plugin/telegram/api_edit_message.go b/plugin/telegram/api_edit_message.go new file mode 100644 index 00000000..aec69873 --- /dev/null +++ b/plugin/telegram/api_edit_message.go @@ -0,0 +1,24 @@ +package telegram + +import ( + "context" + "net/url" + "strconv" +) + +// EditMessage make an editMessageText api request. +func (r *Robot) EditMessage(ctx context.Context, chatID, messageID int, text string) (*Message, error) { + formData := url.Values{ + "message_id": {strconv.Itoa(messageID)}, + "chat_id": {strconv.Itoa(chatID)}, + "text": {text}, + } + + var result Message + err := r.postForm(ctx, "/editMessageText", formData, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/plugin/telegram/api_get_file.go b/plugin/telegram/api_get_file.go new file mode 100644 index 00000000..38e11235 --- /dev/null +++ b/plugin/telegram/api_get_file.go @@ -0,0 +1,21 @@ +package telegram + +import ( + "context" + "net/url" +) + +// GetFile get download info of File by fileID from Telegram. +func (r *Robot) GetFile(ctx context.Context, fileID string) (*File, error) { + formData := url.Values{ + "file_id": {fileID}, + } + + var result File + err := r.postForm(ctx, "/getFile", formData, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/plugin/telegram/api_get_updates.go b/plugin/telegram/api_get_updates.go new file mode 100644 index 00000000..e4a1eed5 --- /dev/null +++ b/plugin/telegram/api_get_updates.go @@ -0,0 +1,23 @@ +package telegram + +import ( + "context" + "net/url" + "strconv" +) + +// GetUpdates make a getUpdates api request. +func (r *Robot) GetUpdates(ctx context.Context, offset int) ([]Update, error) { + formData := url.Values{ + "timeout": {"60"}, + "offset": {strconv.Itoa(offset)}, + } + + var result []Update + err := r.postForm(ctx, "/getUpdates", formData, &result) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/plugin/telegram/api_send_message.go b/plugin/telegram/api_send_message.go new file mode 100644 index 00000000..bf81d864 --- /dev/null +++ b/plugin/telegram/api_send_message.go @@ -0,0 +1,24 @@ +package telegram + +import ( + "context" + "net/url" + "strconv" +) + +// SendReplyMessage make a sendMessage api request. +func (r *Robot) SendReplyMessage(ctx context.Context, chatID, replyID int, text string) (*Message, error) { + formData := url.Values{ + "reply_to_message_id": {strconv.Itoa(replyID)}, + "chat_id": {strconv.Itoa(chatID)}, + "text": {text}, + } + + var result Message + err := r.postForm(ctx, "/sendMessage", formData, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/plugin/telegram/chat.go b/plugin/telegram/chat.go new file mode 100644 index 00000000..bd517216 --- /dev/null +++ b/plugin/telegram/chat.go @@ -0,0 +1,9 @@ +package telegram + +type Chat struct { + ID int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} diff --git a/plugin/telegram/download.go b/plugin/telegram/download.go new file mode 100644 index 00000000..d71b10a6 --- /dev/null +++ b/plugin/telegram/download.go @@ -0,0 +1,44 @@ +package telegram + +import ( + "context" + "fmt" + "io" + "net/http" +) + +// downloadFileId download file with fileID, return the filepath and blob. +func (r *Robot) downloadFileID(ctx context.Context, fileID string) (string, []byte, error) { + file, err := r.GetFile(ctx, fileID) + if err != nil { + return "", nil, err + } + blob, err := r.downloadFilepath(ctx, file.FilePath) + if err != nil { + return "", nil, err + } + + return file.FilePath, blob, nil +} + +// downloadFilepath download file with filepath, you can get filepath by calling GetFile. +func (r *Robot) downloadFilepath(ctx context.Context, filePath string) ([]byte, error) { + token := r.handler.RobotToken(ctx) + if token == "" { + return nil, ErrNoToken + } + + uri := "https://api.telegram.org/file/bot" + token + "/" + filePath + resp, err := http.Get(uri) + if err != nil { + return nil, fmt.Errorf("fail to http.Get: %s", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("fail to io.ReadAll: %s", err) + } + + return body, nil +} diff --git a/plugin/telegram/file.go b/plugin/telegram/file.go new file mode 100644 index 00000000..915cd6d6 --- /dev/null +++ b/plugin/telegram/file.go @@ -0,0 +1,8 @@ +package telegram + +type File struct { + FileID string `json:"file_id"` + FileUniqueID string `json:"file_unique_id"` + FileSize int64 `json:"file_size"` + FilePath string `json:"file_path"` +} diff --git a/plugin/telegram/handle.go b/plugin/telegram/handle.go new file mode 100644 index 00000000..22f85fed --- /dev/null +++ b/plugin/telegram/handle.go @@ -0,0 +1,104 @@ +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 (r *Robot) handleSingleMessage(ctx context.Context, message Message) error { + reply, err := r.SendReplyMessage(ctx, message.Chat.ID, message.MessageID, workingMessage) + 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 := r.downloadFileID(ctx, message.GetMaxPhotoFileID()) + if err != nil { + log.Error("fail to downloadFileID", zap.Error(err)) + _, err = r.EditMessage(ctx, message.Chat.ID, reply.MessageID, err.Error()) + if err != nil { + return fmt.Errorf("fail to EditMessage: %s", err) + } + return fmt.Errorf("fail to downloadFileID: %s", err) + } + blobs = map[string][]byte{filepath: blob} + } + + err = r.handler.MessageHandle(ctx, message, blobs) + if err != nil { + if _, err := r.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 := r.EditMessage(ctx, message.Chat.ID, reply.MessageID, successMessage); err != nil { + return fmt.Errorf("fail to EditMessage: %s", err) + } + + return nil +} + +// handleGroupMessages handle a message belongs to group. +func (r *Robot) handleGroupMessages(ctx context.Context, groupMessages []Message) error { + captions := make(map[string]string, len(groupMessages)) + messages := make(map[string]Message, len(groupMessages)) + blobs := make(map[string]map[string][]byte, len(groupMessages)) + + // Group all captions, blobs and messages + for _, message := range groupMessages { + groupID := *message.MediaGroupID + + messages[groupID] = message + + if message.Caption != nil { + captions[groupID] += *message.Caption + } + + filepath, blob, err := r.downloadFileID(ctx, message.GetMaxPhotoFileID()) + if err != nil { + return fmt.Errorf("fail to downloadFileID") + } + if _, found := blobs[groupID]; !found { + blobs[groupID] = make(map[string][]byte) + } + blobs[groupID][filepath] = blob + } + + // Handle each group message + for groupID, message := range messages { + reply, err := r.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 := r.handler.MessageHandle(ctx, message, blobs[groupID]); err != nil { + if _, err = r.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 := r.EditMessage(ctx, message.Chat.ID, reply.MessageID, successMessage); err != nil { + return fmt.Errorf("fail to EditMessage: %s", err) + } + } + + return nil +} diff --git a/plugin/telegram/message.go b/plugin/telegram/message.go new file mode 100644 index 00000000..1f2e254e --- /dev/null +++ b/plugin/telegram/message.go @@ -0,0 +1,24 @@ +package telegram + +type Message struct { + MessageID int `json:"message_id"` + From User `json:"from"` + Date int `json:"date"` + Text *string `json:"text"` + Chat *Chat `json:"chat"` + MediaGroupID *string `json:"media_group_id"` + Photo []PhotoSize `json:"photo"` + Caption *string `json:"caption"` +} + +func (m Message) GetMaxPhotoFileID() string { + var fileSize int64 + var photoSize PhotoSize + for _, p := range m.Photo { + if p.FileSize > fileSize { + photoSize = p + } + } + + return photoSize.FileID +} diff --git a/plugin/telegram/photosize.go b/plugin/telegram/photosize.go new file mode 100644 index 00000000..d3428115 --- /dev/null +++ b/plugin/telegram/photosize.go @@ -0,0 +1,9 @@ +package telegram + +type PhotoSize struct { + FileID string `json:"file_id"` + FileUniqueID string `json:"file_unique_id"` + FileSize int64 `json:"file_size"` + Width int `json:"width"` + Height int `json:"height"` +} diff --git a/plugin/telegram/request.go b/plugin/telegram/request.go new file mode 100644 index 00000000..21e3e48a --- /dev/null +++ b/plugin/telegram/request.go @@ -0,0 +1,52 @@ +package telegram + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +var ErrNoToken = errors.New("token is empty") + +func (r *Robot) postForm(ctx context.Context, apiPath string, formData url.Values, result any) error { + token := r.handler.RobotToken(ctx) + if token == "" { + return ErrNoToken + } + + uri := "https://api.telegram.org/bot" + token + apiPath + resp, err := http.PostForm(uri, formData) + if err != nil { + return fmt.Errorf("fail to http.PostForm: %s", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("fail to ioutil.ReadAll: %s", err) + } + + var respInfo struct { + Ok bool `json:"ok"` + ErrorCode int `json:"error_code"` + Description string `json:"description"` + Result any `json:"result"` + } + + respInfo.Result = result + + err = json.Unmarshal(body, &respInfo) + if err != nil { + return fmt.Errorf("fail to json.Unmarshal: %s", err) + } + + if !respInfo.Ok { + return fmt.Errorf("api error: [%d]%s", respInfo.ErrorCode, respInfo.Description) + } + + return nil +} diff --git a/plugin/telegram/robot.go b/plugin/telegram/robot.go new file mode 100644 index 00000000..48bc1354 --- /dev/null +++ b/plugin/telegram/robot.go @@ -0,0 +1,79 @@ +package telegram + +import ( + "context" + "fmt" + "time" + + "github.com/usememos/memos/common/log" + "go.uber.org/zap" +) + +type Handler interface { + RobotToken(ctx context.Context) string + MessageHandle(ctx context.Context, message Message, blobs map[string][]byte) error +} + +type Robot struct { + handler Handler +} + +// NewRobotWithHandler create a telegram robot with specified handler. +func NewRobotWithHandler(h Handler) *Robot { + return &Robot{handler: h} +} + +const noTokenWait = 30 * time.Second + +// Start start an infinity call of getUpdates from Telegram, call r.MessageHandle while get new message updates. +func (r *Robot) Start(ctx context.Context) { + var offset int + + for { + updates, err := r.GetUpdates(ctx, offset) + if err == ErrNoToken { + time.Sleep(noTokenWait) + continue + } + if err != nil { + log.Warn("fail to telegram.GetUpdates", zap.Error(err)) + continue + } + + 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 := r.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 + } + + err = r.handleSingleMessage(ctx, message) + if err != nil { + log.Error(fmt.Sprintf("fail to handleSingleMessage for messageID=%d", message.MessageID), zap.Error(err)) + continue + } + } + + err = r.handleGroupMessages(ctx, groupMessages) + if err != nil { + log.Error("fail to handle plain text message", zap.Error(err)) + } + } +} diff --git a/plugin/telegram/update.go b/plugin/telegram/update.go new file mode 100644 index 00000000..e44a3d4b --- /dev/null +++ b/plugin/telegram/update.go @@ -0,0 +1,6 @@ +package telegram + +type Update struct { + UpdateID int `json:"update_id"` + Message *Message `json:"message"` +} diff --git a/plugin/telegram/user.go b/plugin/telegram/user.go new file mode 100644 index 00000000..fc4f5864 --- /dev/null +++ b/plugin/telegram/user.go @@ -0,0 +1,5 @@ +package telegram + +type User struct { + ID int `json:"id"` +} diff --git a/server/memo.go b/server/memo.go index 589c2ab1..f6cbe615 100644 --- a/server/memo.go +++ b/server/memo.go @@ -38,7 +38,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { if createMemoRequest.Visibility == "" { userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{ - UserID: userID, + UserID: &userID, Key: api.UserSettingMemoVisibilityKey, }) if err != nil { @@ -90,7 +90,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err) } - if err := s.createMemoCreateActivity(c, memoMessage); err != nil { + if err := createMemoCreateActivity(c.Request().Context(), s.Store, memoMessage); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) } @@ -503,8 +503,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { }) } -func (s *Server) createMemoCreateActivity(c echo.Context, memo *store.MemoMessage) error { - ctx := c.Request().Context() +func createMemoCreateActivity(ctx context.Context, store *store.Store, memo *store.MemoMessage) error { payload := api.ActivityMemoCreatePayload{ Content: memo.Content, Visibility: memo.Visibility.String(), @@ -513,7 +512,7 @@ func (s *Server) createMemoCreateActivity(c echo.Context, memo *store.MemoMessag if err != nil { return errors.Wrap(err, "failed to marshal activity payload") } - activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{ + activity, err := store.CreateActivity(ctx, &api.ActivityCreate{ CreatorID: memo.CreatorID, Type: api.ActivityMemoCreate, Level: api.ActivityInfo, diff --git a/server/resource.go b/server/resource.go index c506d7f5..5578c4ba 100644 --- a/server/resource.go +++ b/server/resource.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -23,6 +24,7 @@ import ( "github.com/usememos/memos/common" "github.com/usememos/memos/common/log" "github.com/usememos/memos/plugin/storage/s3" + "github.com/usememos/memos/store" "go.uber.org/zap" ) @@ -62,7 +64,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err) } - if err := s.createResourceCreateActivity(c, resource); err != nil { + if err := createResourceCreateActivity(c.Request().Context(), s.Store, resource); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) } return c.JSON(http.StatusOK, composeResponse(resource)) @@ -224,7 +226,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err) } - if err := s.createResourceCreateActivity(c, resource); err != nil { + if err := createResourceCreateActivity(c.Request().Context(), s.Store, resource); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) } return c.JSON(http.StatusOK, composeResponse(resource)) @@ -454,8 +456,7 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) { }) } -func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Resource) error { - ctx := c.Request().Context() +func createResourceCreateActivity(ctx context.Context, store *store.Store, resource *api.Resource) error { payload := api.ActivityResourceCreatePayload{ Filename: resource.Filename, Type: resource.Type, @@ -465,7 +466,7 @@ func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Reso if err != nil { return errors.Wrap(err, "failed to marshal activity payload") } - activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{ + activity, err := store.CreateActivity(ctx, &api.ActivityCreate{ CreatorID: resource.CreatorID, Type: api.ActivityResourceCreate, Level: api.ActivityInfo, diff --git a/server/server.go b/server/server.go index 8ffc3cac..480576da 100644 --- a/server/server.go +++ b/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/usememos/memos/api" + "github.com/usememos/memos/plugin/telegram" "github.com/usememos/memos/server/profile" "github.com/usememos/memos/store" "github.com/usememos/memos/store/db" @@ -24,6 +25,8 @@ type Server struct { ID string Profile *profile.Profile Store *store.Store + + telegramRobot *telegram.Robot } func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) { @@ -45,6 +48,9 @@ func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) { storeInstance := store.New(db.DBInstance, profile) s.Store = storeInstance + telegramRobotHandler := newTelegramHandler(storeInstance) + s.telegramRobot = telegram.NewRobotWithHandler(telegramRobotHandler) + e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: `{"time":"${time_rfc3339}",` + `"method":"${method}","uri":"${uri}",` + @@ -118,6 +124,9 @@ func (s *Server) Start(ctx context.Context) error { if err := s.createServerStartActivity(ctx); err != nil { return errors.Wrap(err, "failed to create activity") } + + go s.telegramRobot.Start(ctx) + return s.e.Start(fmt.Sprintf(":%d", s.Profile.Port)) } diff --git a/server/system.go b/server/system.go index fd852fd8..76eee0d4 100644 --- a/server/system.go +++ b/server/system.go @@ -64,7 +64,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err) } for _, systemSetting := range systemSettingList { - if systemSetting.Name == api.SystemSettingServerIDName || systemSetting.Name == api.SystemSettingSecretSessionName || systemSetting.Name == api.SystemSettingOpenAIConfigName { + if systemSetting.Name == api.SystemSettingServerIDName || systemSetting.Name == api.SystemSettingSecretSessionName || systemSetting.Name == api.SystemSettingOpenAIConfigName || systemSetting.Name == api.SystemSettingTelegramRobotTokenName { continue } diff --git a/server/telegram.go b/server/telegram.go new file mode 100644 index 00000000..db051366 --- /dev/null +++ b/server/telegram.go @@ -0,0 +1,102 @@ +package server + +import ( + "context" + "fmt" + "path" + "strconv" + + "github.com/usememos/memos/api" + "github.com/usememos/memos/common" + "github.com/usememos/memos/plugin/telegram" + "github.com/usememos/memos/store" +) + +type telegramHandler struct { + store *store.Store +} + +func newTelegramHandler(store *store.Store) *telegramHandler { + return &telegramHandler{store: store} +} + +func (t *telegramHandler) RobotToken(ctx context.Context) string { + return t.store.GetSystemSettingValueOrDefault(&ctx, api.SystemSettingTelegramRobotTokenName, "") +} + +func (t *telegramHandler) MessageHandle(ctx context.Context, message telegram.Message, blobs map[string][]byte) error { + 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) + } + for _, userSetting := range userSettingList { + if userSetting.Value == strconv.Itoa(message.From.ID) { + creatorID = userSetting.UserID + } + } + + if creatorID == 0 { + return fmt.Errorf("Please set your telegram userid %d in UserSetting of Memos", message.From.ID) + } + + // create memo + memoCreate := api.CreateMemoRequest{ + CreatorID: creatorID, + Visibility: api.Private, + } + + if message.Text != nil { + memoCreate.Content = *message.Text + } + if blobs != nil && message.Caption != nil { + memoCreate.Content = *message.Caption + } + + memoMessage, err := t.store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(&memoCreate)) + if err != nil { + return fmt.Errorf("failed to CreateMemo: %s", err) + } + + if err := createMemoCreateActivity(ctx, t.store, memoMessage); err != nil { + return fmt.Errorf("failed to createMemoCreateActivity: %s", err) + } + + // create resources + for filename, blob := range blobs { + // TODO support more + mime := "application/octet-stream" + switch path.Ext(filename) { + case ".jpg": + mime = "image/jpeg" + case ".png": + mime = "image/png" + } + resourceCreate := api.ResourceCreate{ + CreatorID: creatorID, + Filename: filename, + Type: mime, + Size: int64(len(blob)), + Blob: blob, + PublicID: common.GenUUID(), + } + resource, err := t.store.CreateResource(ctx, &resourceCreate) + if err != nil { + return fmt.Errorf("failed to CreateResource: %s", err) + } + if err := createResourceCreateActivity(ctx, t.store, resource); err != nil { + return fmt.Errorf("failed to createResourceCreateActivity: %s", err) + } + + _, err = t.store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{ + MemoID: memoMessage.ID, + ResourceID: resource.ID, + }) + if err != nil { + return fmt.Errorf("failed to UpsertMemoResource: %s", err) + } + } + return nil +} diff --git a/server/user.go b/server/user.go index 48174ca6..c1f23b5b 100644 --- a/server/user.go +++ b/server/user.go @@ -116,7 +116,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) { } userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{ - UserID: userID, + UserID: &userID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) @@ -203,7 +203,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) { } userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{ - UserID: userID, + UserID: &userID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) diff --git a/store/user_setting.go b/store/user_setting.go index 641574f4..50a2eb9b 100644 --- a/store/user_setting.go +++ b/store/user_setting.go @@ -124,7 +124,9 @@ func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingF where, args = append(where, "key = ?"), append(args, v) } - where, args = append(where, "user_id = ?"), append(args, find.UserID) + if v := find.UserID; v != nil { + where, args = append(where, "user_id = ?"), append(args, *find.UserID) + } query := ` SELECT