2023-09-19 22:35:20 +08:00
package integration
2023-05-26 09:43:51 +08:00
import (
2023-07-14 11:14:10 +08:00
"bytes"
2023-05-26 09:43:51 +08:00
"context"
2024-03-04 12:24:09 +09:00
"encoding/json"
2023-05-26 09:43:51 +08:00
"fmt"
2023-12-28 20:50:15 -03:00
"path/filepath"
2024-03-12 22:48:53 +08:00
"slices"
2023-05-26 09:43:51 +08:00
"strconv"
2024-02-01 20:24:58 +08:00
"time"
2023-07-13 19:18:44 +03:00
"unicode/utf16"
2023-05-26 09:43:51 +08:00
2024-01-21 10:33:31 +08:00
"github.com/lithammer/shortuuid/v4"
2023-06-26 23:06:53 +08:00
"github.com/pkg/errors"
2024-03-12 22:48:53 +08:00
"github.com/yourselfhosted/gomark/ast"
"github.com/yourselfhosted/gomark/parser"
"github.com/yourselfhosted/gomark/parser/tokenizer"
2023-09-17 22:55:13 +08:00
2023-05-26 09:43:51 +08:00
"github.com/usememos/memos/plugin/telegram"
2024-02-01 20:24:58 +08:00
"github.com/usememos/memos/plugin/webhook"
2023-12-08 22:41:47 +08:00
storepb "github.com/usememos/memos/proto/gen/store"
2024-02-29 01:16:43 +08:00
apiv1 "github.com/usememos/memos/server/route/api/v1"
2024-03-12 22:48:53 +08:00
apiv2 "github.com/usememos/memos/server/route/api/v2"
2023-05-26 09:43:51 +08:00
"github.com/usememos/memos/store"
)
2023-09-19 22:35:20 +08:00
type TelegramHandler struct {
2023-05-26 09:43:51 +08:00
store * store . Store
}
2023-09-19 22:35:20 +08:00
func NewTelegramHandler ( store * store . Store ) * TelegramHandler {
return & TelegramHandler { store : store }
2023-05-26 09:43:51 +08:00
}
2023-09-19 22:35:20 +08:00
func ( t * TelegramHandler ) BotToken ( ctx context . Context ) string {
2024-02-20 23:02:01 +08:00
if setting , err := t . store . GetWorkspaceSetting ( ctx , & store . FindWorkspaceSetting {
Name : apiv1 . SystemSettingTelegramBotTokenName . String ( ) ,
} ) ; err == nil && setting != nil {
return setting . Value
}
return ""
2023-05-26 09:43:51 +08:00
}
2023-06-14 22:10:01 +08:00
const (
2023-08-06 20:26:57 -07:00
workingMessage = "Working on sending your memo..."
2023-06-14 22:10:01 +08:00
successMessage = "Success"
)
2023-09-19 22:35:20 +08:00
func ( t * TelegramHandler ) MessageHandle ( ctx context . Context , bot * telegram . Bot , message telegram . Message , attachments [ ] telegram . Attachment ) error {
2023-06-14 22:10:01 +08:00
reply , err := bot . SendReplyMessage ( ctx , message . Chat . ID , message . MessageID , workingMessage )
if err != nil {
2023-09-17 22:55:13 +08:00
return errors . Wrap ( err , "Failed to SendReplyMessage" )
2023-06-14 22:10:01 +08:00
}
2024-03-12 22:48:53 +08:00
messageSenderID := strconv . FormatInt ( message . From . ID , 10 )
2023-08-04 21:55:07 +08:00
var creatorID int32
2023-12-16 12:18:53 +08:00
userSettingList , err := t . store . ListUserSettings ( ctx , & store . FindUserSetting {
2023-12-08 22:41:47 +08:00
Key : storepb . UserSettingKey_USER_SETTING_TELEGRAM_USER_ID ,
2023-05-26 09:43:51 +08:00
} )
if err != nil {
2023-06-26 23:06:53 +08:00
return errors . Wrap ( err , "Failed to find userSettingList" )
2023-05-26 09:43:51 +08:00
}
2023-06-29 22:55:03 +08:00
for _ , userSetting := range userSettingList {
2024-03-12 22:48:53 +08:00
if userSetting . GetTelegramUserId ( ) == messageSenderID {
2023-12-08 22:41:47 +08:00
creatorID = userSetting . UserId
2023-05-26 09:43:51 +08:00
}
}
2024-03-12 22:48:53 +08:00
// If creatorID is not found, ask the user to set the telegram userid in UserSetting of memos.
2023-05-26 09:43:51 +08:00
if creatorID == 0 {
2023-10-18 06:05:19 +08:00
_ , 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 )
2023-06-14 22:10:01 +08:00
return err
2023-05-26 09:43:51 +08:00
}
2023-07-06 21:56:42 +08:00
create := & store . Memo {
2024-03-20 20:39:16 +08:00
UID : shortuuid . New ( ) ,
CreatorID : creatorID ,
Visibility : store . Private ,
2023-05-26 09:43:51 +08:00
}
if message . Text != nil {
2023-07-13 19:18:44 +03:00
create . Content = convertToMarkdown ( * message . Text , message . Entities )
2023-05-26 09:43:51 +08:00
}
2023-07-13 19:18:44 +03:00
if message . Caption != nil {
create . Content = convertToMarkdown ( * message . Caption , message . CaptionEntities )
}
if message . ForwardFromChat != nil {
create . Content += fmt . Sprintf ( "\n\n[Message link](%s)" , message . GetMessageLink ( ) )
2023-05-26 09:43:51 +08:00
}
2023-07-06 21:56:42 +08:00
memoMessage , err := t . store . CreateMemo ( ctx , create )
2023-05-26 09:43:51 +08:00
if err != nil {
2023-08-06 20:26:57 -07:00
_ , err := bot . EditMessage ( ctx , message . Chat . ID , reply . MessageID , fmt . Sprintf ( "Failed to CreateMemo: %s" , err ) , nil )
2023-06-14 22:10:01 +08:00
return err
2023-05-26 09:43:51 +08:00
}
2024-03-12 22:48:53 +08:00
// Dynamically upsert tags from memo content.
nodes , err := parser . Parse ( tokenizer . Tokenize ( create . Content ) )
if err != nil {
return errors . Wrap ( err , "Failed to parse content" )
}
tags := [ ] string { }
apiv2 . TraverseASTNodes ( nodes , func ( node ast . Node ) {
if tagNode , ok := node . ( * ast . Tag ) ; ok {
tag := tagNode . Content
if ! slices . Contains ( tags , tag ) {
tags = append ( tags , tag )
}
}
} )
for _ , tag := range tags {
_ , err := t . store . UpsertTag ( ctx , & store . Tag {
Name : tag ,
CreatorID : creatorID ,
} )
if err != nil {
return errors . Wrap ( err , "Failed to upsert tag" )
}
}
// Create memo related resources.
2023-07-13 19:18:44 +03:00
for _ , attachment := range attachments {
2023-07-14 11:14:10 +08:00
// Fill the common field of create
create := store . Resource {
2024-03-20 20:39:16 +08:00
UID : shortuuid . New ( ) ,
CreatorID : creatorID ,
Filename : filepath . Base ( attachment . FileName ) ,
Type : attachment . GetMimeType ( ) ,
Size : attachment . FileSize ,
MemoID : & memoMessage . ID ,
2023-07-14 11:14:10 +08:00
}
err := apiv1 . SaveResourceBlob ( ctx , t . store , & create , bytes . NewReader ( attachment . Data ) )
if err != nil {
2023-08-06 20:26:57 -07:00
_ , err := bot . EditMessage ( ctx , message . Chat . ID , reply . MessageID , fmt . Sprintf ( "Failed to SaveResourceBlob: %s" , err ) , nil )
2023-07-14 11:14:10 +08:00
return err
}
2023-09-27 00:40:16 +08:00
_ , err = t . store . CreateResource ( ctx , & create )
2023-05-26 09:43:51 +08:00
if err != nil {
2023-08-06 20:26:57 -07:00
_ , err := bot . EditMessage ( ctx , message . Chat . ID , reply . MessageID , fmt . Sprintf ( "Failed to CreateResource: %s" , err ) , nil )
2023-06-14 22:10:01 +08:00
return err
2023-05-26 09:43:51 +08:00
}
2023-06-14 22:10:01 +08:00
}
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 )
2024-02-01 20:24:58 +08:00
_ = t . dispatchMemoRelatedWebhook ( ctx , * memoMessage , "memos.memo.created" )
2023-06-14 22:10:01 +08:00
return err
}
2023-09-19 22:35:20 +08:00
func ( t * TelegramHandler ) CallbackQueryHandle ( ctx context . Context , bot * telegram . Bot , callbackQuery telegram . CallbackQuery ) error {
2023-08-04 21:55:07 +08:00
var memoID int32
2023-06-14 22:10:01 +08:00
var visibility store . Visibility
n , err := fmt . Sscanf ( callbackQuery . Data , "%s %d" , & visibility , & memoID )
if err != nil || n != 2 {
2023-08-06 20:26:57 -07:00
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to parse callbackQuery.Data %s" , callbackQuery . Data ) )
2023-06-14 22:10:01 +08:00
}
2024-02-27 00:03:08 +08:00
memo , err := t . store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
} )
if err != nil {
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to call FindMemo %s" , err ) )
}
if memo == nil {
_ , err = bot . EditMessage ( ctx , callbackQuery . Message . Chat . ID , callbackQuery . Message . MessageID , fmt . Sprintf ( "Memo %d not found" , memoID ) , nil )
if err != nil {
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to EditMessage %s" , err ) )
}
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Memo %d not found, possibly deleted elsewhere" , memoID ) )
}
2024-03-04 12:24:09 +09:00
setting , err := t . store . GetWorkspaceSetting ( ctx , & store . FindWorkspaceSetting {
Name : apiv1 . SystemSettingDisablePublicMemosName . String ( ) ,
} )
if err != nil {
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to get workspace setting %s" , err ) )
}
2024-03-09 12:56:44 +08:00
if setting != nil && setting . Value != "" {
disablePublicMemo := false
err = json . Unmarshal ( [ ] byte ( setting . Value ) , & disablePublicMemo )
if err != nil {
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to get workspace setting %s" , err ) )
}
if disablePublicMemo && visibility == store . Public {
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to changing Memo %d to %s\n(workspace disallowed public memo)" , memoID , visibility ) )
}
2024-03-04 12:24:09 +09:00
}
2023-07-06 21:56:42 +08:00
update := store . UpdateMemo {
2023-06-14 22:10:01 +08:00
ID : memoID ,
Visibility : & visibility ,
}
err = t . store . UpdateMemo ( ctx , & update )
if err != nil {
2023-08-06 20:26:57 -07:00
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to call UpdateMemo %s" , err ) )
2023-06-14 22:10:01 +08:00
}
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 {
2023-08-06 20:26:57 -07:00
return bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Failed to EditMessage %s" , err ) )
2023-06-14 22:10:01 +08:00
}
2024-02-01 20:24:58 +08:00
err = bot . AnswerCallbackQuery ( ctx , callbackQuery . ID , fmt . Sprintf ( "Success changing Memo %d to %s" , memoID , visibility ) )
memo , webhookErr := t . store . GetMemo ( ctx , & store . FindMemo {
ID : & memoID ,
} )
if webhookErr == nil {
_ = t . dispatchMemoRelatedWebhook ( ctx , * memo , "memos.memo.updated" )
}
return err
2023-06-14 22:10:01 +08:00
}
2023-08-04 21:55:07 +08:00
func generateKeyboardForMemoID ( id int32 ) [ ] [ ] telegram . InlineKeyboardButton {
2023-06-14 22:10:01 +08:00
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 ) ,
2023-05-26 09:43:51 +08:00
}
2023-06-14 22:10:01 +08:00
buttons = append ( buttons , button )
2023-05-26 09:43:51 +08:00
}
2023-06-14 22:10:01 +08:00
return [ ] [ ] telegram . InlineKeyboardButton { buttons }
2023-05-26 09:43:51 +08:00
}
2023-07-13 19:18:44 +03:00
func convertToMarkdown ( text string , messageEntities [ ] telegram . MessageEntity ) string {
insertions := make ( map [ int ] string )
for _ , e := range messageEntities {
var before , after string
// this is supported by the current markdown
switch e . Type {
case telegram . Bold :
before = "**"
after = "**"
case telegram . Italic :
before = "*"
after = "*"
case telegram . Strikethrough :
before = "~~"
after = "~~"
case telegram . Code :
before = "`"
after = "`"
case telegram . Pre :
before = "```" + e . Language
after = "```"
case telegram . TextLink :
before = "["
after = fmt . Sprintf ( ` ](%s) ` , e . URL )
2024-03-12 22:48:53 +08:00
case telegram . Spoiler :
before = "||"
after = "||"
2023-07-13 19:18:44 +03:00
}
if before != "" {
insertions [ e . Offset ] += before
insertions [ e . Offset + e . Length ] = after + insertions [ e . Offset + e . Length ]
}
}
input := [ ] rune ( text )
var output [ ] rune
utf16pos := 0
for i := 0 ; i < len ( input ) ; i ++ {
output = append ( output , [ ] rune ( insertions [ utf16pos ] ) ... )
output = append ( output , input [ i ] )
utf16pos += len ( utf16 . Encode ( [ ] rune { input [ i ] } ) )
}
output = append ( output , [ ] rune ( insertions [ utf16pos ] ) ... )
return string ( output )
}
2024-02-01 20:24:58 +08:00
func ( t * TelegramHandler ) dispatchMemoRelatedWebhook ( ctx context . Context , memo store . Memo , activityType string ) error {
webhooks , err := t . store . ListWebhooks ( ctx , & store . FindWebhook {
CreatorID : & memo . CreatorID ,
} )
if err != nil {
return err
}
for _ , hook := range webhooks {
payload := t . convertMemoToWebhookPayload ( ctx , memo )
payload . ActivityType = activityType
payload . URL = hook . Url
err := webhook . Post ( * payload )
if err != nil {
return errors . Wrap ( err , "failed to post webhook" )
}
}
return nil
}
func ( t * TelegramHandler ) convertMemoToWebhookPayload ( ctx context . Context , memo store . Memo ) ( payload * webhook . WebhookPayload ) {
payload = & webhook . WebhookPayload {
CreatorID : memo . CreatorID ,
CreatedTs : time . Now ( ) . Unix ( ) ,
Memo : & webhook . Memo {
ID : memo . ID ,
CreatorID : memo . CreatorID ,
CreatedTs : memo . CreatedTs ,
UpdatedTs : memo . UpdatedTs ,
Content : memo . Content ,
Visibility : memo . Visibility . String ( ) ,
Pinned : memo . Pinned ,
ResourceList : make ( [ ] * webhook . Resource , 0 ) ,
RelationList : make ( [ ] * webhook . MemoRelation , 0 ) ,
} ,
}
resourceList , err := t . store . ListResources ( ctx , & store . FindResource {
MemoID : & memo . ID ,
} )
if err != nil {
return payload
}
for _ , resource := range resourceList {
payload . Memo . ResourceList = append ( payload . Memo . ResourceList , & webhook . Resource {
ID : resource . ID ,
CreatorID : resource . CreatorID ,
CreatedTs : resource . CreatedTs ,
UpdatedTs : resource . UpdatedTs ,
Filename : resource . Filename ,
Type : resource . Type ,
Size : resource . Size ,
InternalPath : resource . InternalPath ,
ExternalLink : resource . ExternalLink ,
} )
}
relationList , err := t . store . ListMemoRelations ( ctx , & store . FindMemoRelation {
MemoID : & memo . ID ,
} )
if err != nil {
return payload
}
for _ , relation := range relationList {
payload . Memo . RelationList = append ( payload . Memo . RelationList , & webhook . MemoRelation {
MemoID : relation . MemoID ,
RelatedMemoID : relation . RelatedMemoID ,
Type : string ( relation . Type ) ,
} )
}
return payload
}