diff --git a/bridgev2/networkinterface.go b/bridgev2/networkinterface.go index b42e730f..1e943d4d 100644 --- a/bridgev2/networkinterface.go +++ b/bridgev2/networkinterface.go @@ -635,6 +635,8 @@ func (ret RemoteEventType) String() string { return "RemoteEventReaction" case RemoteEventReactionRemove: return "RemoteEventReactionRemove" + case RemoteEventReactionSync: + return "RemoteEventReactionSync" case RemoteEventMessageRemove: return "RemoteEventMessageRemove" case RemoteEventReadReceipt: @@ -664,6 +666,7 @@ const ( RemoteEventEdit RemoteEventReaction RemoteEventReactionRemove + RemoteEventReactionSync RemoteEventMessageRemove RemoteEventReadReceipt RemoteEventDeliveryReceipt @@ -751,6 +754,23 @@ type RemoteReaction interface { GetReactionEmoji() (string, networkid.EmojiID) } +type ReactionSyncUser struct { + Reactions []*BackfillReaction + // Whether the list contains all reactions the user has sent + HasAllReactions bool +} + +type ReactionSyncData struct { + Users map[networkid.UserID]*ReactionSyncUser + // Whether the map contains all users who have reacted to the message + HasAllUsers bool +} + +type RemoteReactionSync interface { + RemoteEventWithTargetMessage + GetReactions() *ReactionSyncData +} + type RemoteReactionWithExtraContent interface { RemoteReaction GetReactionExtraContent() map[string]any diff --git a/bridgev2/portal.go b/bridgev2/portal.go index c374c5ca..c02c6bf9 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -1235,6 +1235,8 @@ func (portal *Portal) handleRemoteEvent(source *UserLogin, evt RemoteEvent) { portal.handleRemoteReaction(ctx, source, evt.(RemoteReaction)) case RemoteEventReactionRemove: portal.handleRemoteReactionRemove(ctx, source, evt.(RemoteReactionRemove)) + case RemoteEventReactionSync: + portal.handleRemoteReactionSync(ctx, source, evt.(RemoteReactionSync)) case RemoteEventMessageRemove: portal.handleRemoteMessageRemove(ctx, source, evt.(RemoteMessageRemove)) case RemoteEventReadReceipt: @@ -1569,6 +1571,109 @@ func getEventTS(evt RemoteEvent) time.Time { return time.Now() } +func (portal *Portal) handleRemoteReactionSync(ctx context.Context, source *UserLogin, evt RemoteReactionSync) { + log := zerolog.Ctx(ctx) + eventTS := getEventTS(evt) + targetMessage, err := portal.getTargetMessagePart(ctx, evt) + if err != nil { + log.Err(err).Msg("Failed to get target message for reaction") + return + } else if targetMessage == nil { + // TODO use deterministic event ID as target if applicable? + log.Warn().Msg("Target message for reaction not found") + return + } + var existingReactions []*database.Reaction + if partTargeter, ok := evt.(RemoteEventWithTargetPart); ok { + existingReactions, err = portal.Bridge.DB.Reaction.GetAllToMessagePart(ctx, evt.GetTargetMessage(), partTargeter.GetTargetMessagePart()) + } else { + existingReactions, err = portal.Bridge.DB.Reaction.GetAllToMessage(ctx, evt.GetTargetMessage()) + } + existing := make(map[networkid.UserID]map[networkid.EmojiID]*database.Reaction) + for _, existingReaction := range existingReactions { + if existing[existingReaction.SenderID] == nil { + existing[existingReaction.SenderID] = make(map[networkid.EmojiID]*database.Reaction) + } + existing[existingReaction.SenderID][existingReaction.EmojiID] = existingReaction + } + + doAddReaction := func(new *BackfillReaction) MatrixAPI { + intent := portal.GetIntentFor(ctx, new.Sender, source, RemoteEventReactionSync) + portal.sendConvertedReaction( + ctx, new.Sender.Sender, intent, targetMessage, new.EmojiID, new.Emoji, + new.Timestamp, new.DBMetadata, new.ExtraContent, + func(z *zerolog.Event) *zerolog.Event { + return z. + Any("reaction_sender_id", new.Sender). + Time("reaction_ts", new.Timestamp) + }, + ) + return intent + } + doRemoveReaction := func(old *database.Reaction, intent MatrixAPI) { + if intent == nil && old.SenderMXID != "" { + intent, err = portal.getIntentForMXID(ctx, old.SenderMXID) + if err != nil { + log.Err(err). + Stringer("reaction_sender_mxid", old.SenderMXID). + Msg("Failed to get intent for removing reaction") + } + } + if intent == nil { + log.Warn(). + Str("reaction_sender_id", string(old.SenderID)). + Stringer("reaction_sender_mxid", old.SenderMXID). + Msg("Didn't find intent for removing reaction, using bridge bot") + intent = portal.Bridge.Bot + } + _, err = intent.SendMessage(ctx, portal.MXID, event.EventRedaction, &event.Content{ + Parsed: &event.RedactionEventContent{ + Redacts: old.MXID, + }, + }, &MatrixSendExtra{Timestamp: eventTS}) + if err != nil { + log.Err(err).Msg("Failed to redact old reaction") + } + } + doOverwriteReaction := func(new *BackfillReaction, old *database.Reaction) { + intent := doAddReaction(new) + doRemoveReaction(old, intent) + } + + newData := evt.GetReactions() + for userID, reactions := range newData.Users { + existingUserReactions := existing[userID] + delete(existing, userID) + for _, reaction := range reactions.Reactions { + if reaction.Timestamp.IsZero() { + reaction.Timestamp = eventTS + } + existingReaction, ok := existingUserReactions[reaction.EmojiID] + if ok { + delete(existingUserReactions, reaction.EmojiID) + if reaction.EmojiID != "" { + continue + } + doOverwriteReaction(reaction, existingReaction) + } else { + doAddReaction(reaction) + } + } + if reactions.HasAllReactions { + for _, existingReaction := range existingUserReactions { + doRemoveReaction(existingReaction, nil) + } + } + } + if newData.HasAllUsers { + for _, userReactions := range existing { + for _, existingReaction := range userReactions { + doRemoveReaction(existingReaction, nil) + } + } + } +} + func (portal *Portal) handleRemoteReaction(ctx context.Context, source *UserLogin, evt RemoteReaction) { log := zerolog.Ctx(ctx) targetMessage, err := portal.getTargetMessagePart(ctx, evt)