From 7e680f1fee778a29c8341c4604a260550f79c3c2 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Fri, 5 Jul 2024 17:45:37 -0600 Subject: [PATCH] reactions: support deletions Signed-off-by: Sumner Evans --- pkg/connector/client.go | 52 ++++++++++++----------- pkg/connector/ids/ids.go | 4 ++ pkg/connector/matrix.go | 79 ++++++++++++++++++++++++++++------- pkg/connector/telegram.go | 64 ++++++++++++++++++++-------- pkg/connector/tljson/parse.go | 40 ++++++++++++++++++ 5 files changed, 181 insertions(+), 58 deletions(-) create mode 100644 pkg/connector/tljson/parse.go diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 1088adc9..c6ac0609 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "strings" "sync" @@ -24,14 +23,19 @@ import ( ) type TelegramClient struct { - main *TelegramConnector - loginID int64 - userLogin *bridgev2.UserLogin - client *telegram.Client - clientCancel context.CancelFunc - msgConv *msgconv.MessageConverter + main *TelegramConnector + telegramUserID int64 + loginID networkid.UserLoginID + userID networkid.UserID + userLogin *bridgev2.UserLogin + client *telegram.Client + clientCancel context.CancelFunc + msgConv *msgconv.MessageConverter reactionMessageLocks map[int]*sync.Mutex + + appConfig map[string]any + appConfigHash int } var ( @@ -74,22 +78,24 @@ func (u UpdateDispatcher) Handle(ctx context.Context, updates tg.UpdatesClass) e } func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridgev2.UserLogin) (*TelegramClient, error) { - loginID, err := strconv.ParseInt(string(login.ID), 10, 64) + telegramUserID, err := ids.ParseUserLoginID(login.ID) if err != nil { return nil, err } log := zerolog.Ctx(ctx).With(). Str("component", "telegram_client"). - Int64("login_id", loginID). + Str("user_login_id", string(login.ID)). Logger() zaplog := zap.New(zerozap.New(log)) client := TelegramClient{ - main: tc, - loginID: loginID, - userLogin: login, + main: tc, + telegramUserID: telegramUserID, + loginID: login.ID, + userID: networkid.UserID(login.ID), + userLogin: login, } dispatcher := UpdateDispatcher{ UpdateDispatcher: tg.NewUpdateDispatcher(), @@ -101,7 +107,7 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge dispatcher.OnDeleteMessages(client.onDeleteMessages) dispatcher.OnEditMessage(client.onMessageEdit) - store := tc.Store.GetScopedStore(loginID) + store := tc.Store.GetScopedStore(telegramUserID) updatesManager := updates.New(updates.Config{ OnChannelTooLong: func(channelID int64) { @@ -123,7 +129,7 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge client.clientCancel, err = connectTelegramClient(ctx, client.client) client.reactionMessageLocks = map[int]*sync.Mutex{} go func() { - err = updatesManager.Run(ctx, client.client.API(), loginID, updates.AuthOptions{}) + err = updatesManager.Run(ctx, client.client.API(), telegramUserID, updates.AuthOptions{}) if err != nil { log.Err(err).Msg("updates manager error") client.clientCancel() @@ -165,9 +171,6 @@ func connectTelegramClient(ctx context.Context, client *telegram.Client) (contex return cancel, nil } - - - func (t *TelegramClient) Connect(ctx context.Context) (err error) { t.clientCancel, err = connectTelegramClient(ctx, t.client) return @@ -213,8 +216,8 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta { EventSender: bridgev2.EventSender{ IsFromMe: true, - SenderLogin: ids.MakeUserLoginID(t.loginID), - Sender: ids.MakeUserID(t.loginID), + SenderLogin: t.loginID, + Sender: t.userID, }, }, } @@ -250,7 +253,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta for _, user := range fullChat.Users { memberList.Members = append(memberList.Members, bridgev2.ChatMember{ EventSender: bridgev2.EventSender{ - IsFromMe: user.GetID() == t.loginID, + IsFromMe: user.GetID() == t.telegramUserID, SenderLogin: ids.MakeUserLoginID(user.GetID()), Sender: ids.MakeUserID(user.GetID()), }, @@ -316,10 +319,11 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(user *tg.User) (*bridgev2.U name := util.FormatFullName(user.FirstName, user.LastName) return &bridgev2.UserInfo{ - IsBot: &user.Bot, - Name: &name, - Avatar: avatar, - Identifiers: identifiers, + IsBot: &user.Bot, + Name: &name, + Avatar: avatar, + Identifiers: identifiers, + ExtraUpdates: bridgev2.SimpleMetadataUpdater[*bridgev2.Ghost]("fi.mau.telegram.is_premium", user.Premium), }, nil } diff --git a/pkg/connector/ids/ids.go b/pkg/connector/ids/ids.go index df9c8dbd..3a20994a 100644 --- a/pkg/connector/ids/ids.go +++ b/pkg/connector/ids/ids.go @@ -17,6 +17,10 @@ func ParseUserID(userID networkid.UserID) (int64, error) { return strconv.ParseInt(string(userID), 10, 64) } +func ParseUserLoginID(userID networkid.UserLoginID) (int64, error) { + return strconv.ParseInt(string(userID), 10, 64) +} + func MakeUserLoginID(userID int64) networkid.UserLoginID { return networkid.UserLoginID(strconv.FormatInt(userID, 10)) } diff --git a/pkg/connector/matrix.go b/pkg/connector/matrix.go index a775a6e5..c84f4631 100644 --- a/pkg/connector/matrix.go +++ b/pkg/connector/matrix.go @@ -157,7 +157,7 @@ func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2. ID: ids.MakeMessageID(tgMessageID), MXID: msg.Event.ID, Room: networkid.PortalKey{ID: msg.Portal.ID}, - SenderID: ids.MakeUserID(t.loginID), + SenderID: t.userID, Timestamp: time.Unix(int64(tgDate), 0), }, } @@ -182,9 +182,8 @@ func (t *TelegramClient) HandleMatrixMessageRemove(ctx context.Context, msg *bri func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { var resp bridgev2.MatrixReactionPreResponse - sender := ids.MakeUserID(t.loginID) var maxReactions int - maxReactions, err := t.getReactionLimit(ctx, sender) + maxReactions, err := t.getReactionLimit(ctx, t.userID) if err != nil { return resp, err } @@ -207,13 +206,23 @@ func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridg } return bridgev2.MatrixReactionPreResponse{ - SenderID: sender, + SenderID: t.userID, EmojiID: emojiID, Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key), MaxReactions: maxReactions, }, nil } +func (t *TelegramClient) appendEmojiID(reactionList []tg.ReactionClass, emojiID networkid.EmojiID) ([]tg.ReactionClass, error) { + if documentID, emoticon, err := ids.ParseEmojiID(emojiID); err != nil { + return nil, err + } else if documentID > 0 { + return append(reactionList, &tg.ReactionCustomEmoji{DocumentID: documentID}), nil + } else { + return append(reactionList, &tg.ReactionEmoji{Emoticon: emoticon}), nil + } +} + func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) { peer, err := ids.InputPeerForPortalID(msg.Portal.ID) if err != nil { @@ -224,25 +233,63 @@ func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2 return nil, err } - req := tg.MessagesSendReactionRequest{ + var newReactions []tg.ReactionClass + for _, existing := range msg.ExistingReactionsToKeep { + newReactions, err = t.appendEmojiID(newReactions, existing.EmojiID) + if err != nil { + return nil, err + } + } + newReactions, err = t.appendEmojiID(newReactions, msg.PreHandleResp.EmojiID) + if err != nil { + return nil, err + } + + _, err = t.client.API().MessagesSendReaction(ctx, &tg.MessagesSendReactionRequest{ Peer: peer, AddToRecent: true, MsgID: targetMessageID, - } - if documentID, emoticon, err := ids.ParseEmojiID(msg.PreHandleResp.EmojiID); err != nil { - return nil, err - } else if documentID > 0 { - req.SetReaction([]tg.ReactionClass{&tg.ReactionCustomEmoji{DocumentID: documentID}}) - } else { - req.SetReaction([]tg.ReactionClass{&tg.ReactionEmoji{Emoticon: emoticon}}) - } - - _, err = t.client.API().MessagesSendReaction(ctx, &req) + Reaction: newReactions, + }) return &database.Reaction{}, err } func (t *TelegramClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error { - panic("unimplemented reaction remove") + peer, err := ids.InputPeerForPortalID(msg.Portal.ID) + if err != nil { + return err + } + + var newReactions []tg.ReactionClass + + if maxReactions, err := t.getReactionLimit(ctx, t.userID); err != nil { + return err + } else if maxReactions > 1 { + existing, err := t.main.Bridge.DB.Reaction.GetAllToMessageBySender(ctx, msg.TargetReaction.MessageID, msg.TargetReaction.SenderID) + if err != nil { + return err + } + for _, existing := range existing { + if msg.TargetReaction.EmojiID != existing.EmojiID { + newReactions, err = t.appendEmojiID(newReactions, existing.EmojiID) + if err != nil { + return err + } + } + } + } + + messageID, err := ids.ParseMessageID(msg.TargetReaction.MessageID) + if err != nil { + return err + } + _, err = t.client.API().MessagesSendReaction(ctx, &tg.MessagesSendReactionRequest{ + Peer: peer, + AddToRecent: true, + MsgID: messageID, + Reaction: newReactions, + }) + return err } func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridgev2.MatrixReadReceipt) error { diff --git a/pkg/connector/telegram.go b/pkg/connector/telegram.go index 225ce9d5..ba3da241 100644 --- a/pkg/connector/telegram.go +++ b/pkg/connector/telegram.go @@ -16,6 +16,7 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/emojis" "go.mau.fi/mautrix-telegram/pkg/connector/ids" "go.mau.fi/mautrix-telegram/pkg/connector/media" + "go.mau.fi/mautrix-telegram/pkg/connector/tljson" "go.mau.fi/mautrix-telegram/pkg/connector/util" ) @@ -119,8 +120,8 @@ type messageWithSender interface { func (t *TelegramClient) getEventSender(msg messageWithSender) (sender bridgev2.EventSender) { if msg.GetOut() { sender.IsFromMe = true - sender.SenderLogin = ids.MakeUserLoginID(t.loginID) - sender.Sender = ids.MakeUserID(t.loginID) + sender.SenderLogin = t.loginID + sender.Sender = t.userID } else if f, ok := msg.GetFromID(); ok { switch from := f.(type) { case *tg.PeerUser: @@ -160,7 +161,7 @@ func (t *TelegramClient) onUserName(ctx context.Context, e tg.Entities, update * func (t *TelegramClient) onDeleteMessages(ctx context.Context, e tg.Entities, update *tg.UpdateDeleteMessages) error { for _, messageID := range update.Messages { - parts, err := t.main.Bridge.DB.Message.GetAllPartsByID(ctx, ids.MakeUserLoginID(t.loginID), ids.MakeMessageID(messageID)) + parts, err := t.main.Bridge.DB.Message.GetAllPartsByID(ctx, t.loginID, ids.MakeMessageID(messageID)) if err != nil { return err } @@ -243,7 +244,7 @@ func (t *TelegramClient) handleTelegramReactions(ctx context.Context, msg *tg.Me // self.log.warning(f"Can see reaction list in channel ({data!s})") // # return - dbMsg, err := t.main.Bridge.DB.Message.GetFirstPartByID(ctx, ids.MakeUserLoginID(t.loginID), ids.MakeMessageID(msg.ID)) + dbMsg, err := t.main.Bridge.DB.Message.GetFirstPartByID(ctx, t.loginID, ids.MakeMessageID(msg.ID)) if err != nil { return err } else if dbMsg == nil { @@ -252,7 +253,7 @@ func (t *TelegramClient) handleTelegramReactions(ctx context.Context, msg *tg.Me if len(reactionsList) < totalCount { if user, ok := msg.PeerID.(*tg.PeerUser); ok { - reactionsList = splitDMReactionCounts(msg.Reactions.Results, user.UserID, t.loginID) + reactionsList = splitDMReactionCounts(msg.Reactions.Results, user.UserID, t.telegramUserID) // TODO // } else if t.isBot { @@ -318,13 +319,44 @@ func splitDMReactionCounts(res []tg.ReactionCount, theirUserID, myUserID int64) return } -func (t *TelegramClient) getReactionLimit(ctx context.Context, sender networkid.UserID) (int, error) { - // TODO implement this correctly (probably need to put something into metadata) - // ghost, err := t.main.Bridge.GetGhostByID(ctx, sender) - // if err != nil { - // return 0, err - // } - return 1, nil +func (t *TelegramClient) getAppConfigCached(ctx context.Context) (map[string]any, error) { + if t.appConfig == nil { + cfg, err := t.client.API().HelpGetAppConfig(ctx, t.appConfigHash) + if err != nil { + return nil, err + } + appConfig, ok := cfg.(*tg.HelpAppConfig) + if !ok { + return nil, fmt.Errorf("failed to get app config: unexpected type %T", appConfig) + } + parsedConfig, err := tljson.Parse(appConfig.Config) + if err != nil { + return nil, err + } + t.appConfig, ok = parsedConfig.(map[string]any) + if !ok { + return nil, fmt.Errorf("failed to parse app config: unexpected type %T", t.appConfig) + } + t.appConfigHash = appConfig.Hash + } + return t.appConfig, nil +} + +func (t *TelegramClient) getReactionLimit(ctx context.Context, sender networkid.UserID) (limit int, err error) { + config, err := t.getAppConfigCached(ctx) + if err != nil { + return 0, err + } + + ghost, err := t.main.Bridge.GetGhostByID(ctx, sender) + if err != nil { + return 0, err + } + if isPremium, ok := ghost.Metadata.Extra["fi.mau.telegram.is_premium"].(bool); ok && isPremium { + return int(config["reactions_user_max_premium"].(float64)), nil + } else { + return int(config["reactions_user_max_default"].(float64)), nil + } } func (t *TelegramClient) transferEmojisToMatrix(ctx context.Context, customEmojiIDs []int64) (result map[networkid.EmojiID]string, err error) { @@ -430,10 +462,6 @@ func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context } for _, r := range removed { - senderID, err := ids.ParseUserID(r.SenderID) - if err != nil { - return err - } evt := &bridgev2.SimpleRemoteEvent[any]{ Type: bridgev2.RemoteEventReactionRemove, LogContext: func(c zerolog.Context) zerolog.Context { @@ -443,8 +471,8 @@ func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context Str("message_id", string(msg.ID)) }, Sender: bridgev2.EventSender{ - IsFromMe: t.loginID == senderID, - SenderLogin: ids.MakeUserLoginID(senderID), + IsFromMe: t.userID == r.SenderID, + SenderLogin: t.loginID, Sender: r.SenderID, }, PortalKey: msg.Room, diff --git a/pkg/connector/tljson/parse.go b/pkg/connector/tljson/parse.go new file mode 100644 index 00000000..ddc06879 --- /dev/null +++ b/pkg/connector/tljson/parse.go @@ -0,0 +1,40 @@ +package tljson + +import ( + "fmt" + + "github.com/gotd/td/tg" +) + +func Parse(v tg.JSONValueClass) (out any, err error) { + switch val := v.(type) { + case *tg.JSONBool: + return val.Value, nil + case *tg.JSONNumber: + return val.Value, nil + case *tg.JSONString: + return val.Value, nil + case *tg.JSONArray: + out := make([]any, len(val.Value)) + for i, entry := range val.Value { + out[i], err = Parse(entry) + if err != nil { + return nil, err + } + } + return out, nil + case *tg.JSONObject: + out := make(map[string]any, len(val.Value)) + for _, entry := range val.Value { + out[entry.Key], err = Parse(entry.Value) + if err != nil { + return nil, err + } + } + return out, nil + case *tg.JSONNull: + return nil, nil + default: + return nil, fmt.Errorf("unknown JSON value type %T", v) + } +}