Skip to content

Commit cf5e471

Browse files
authored
fix: allow reusing LINE custom reactions (#227)
* fix: allow reusing LINE custom reactions * fix: skip invalid stored reactions
1 parent 67e653a commit cf5e471

5 files changed

Lines changed: 450 additions & 135 deletions

File tree

pkg/connector/client.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ type LineClient struct {
8080
knownMemberChatMIDs map[string]struct{} // chatMid -> current member chats returned by getAllChatMids
8181
reactionIconMXC map[int]string // predefinedReactionType -> cached MXC URI
8282
paidReactionIconMXC map[string]string // LINE sticon URL -> cached MXC URI
83-
recentReactions sync.Map // "msgID\x00emoji" -> struct{} to dedup concurrent 139/140 events
8483
unblockBackfills sync.Map // chat MID -> *unblockBackfillState while unblock history restoration is active
8584

8685
wg sync.WaitGroup

pkg/connector/connector.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,12 @@ func (lc *LineConnector) GetConfig() (example string, data any, upgrader configu
9999

100100
func (lc *LineConnector) GetDBMetaTypes() database.MetaTypes {
101101
return database.MetaTypes{
102-
Portal: nil,
103-
Ghost: nil,
104-
Message: nil,
105-
Reaction: nil,
102+
Portal: nil,
103+
Ghost: nil,
104+
Message: nil,
105+
Reaction: func() any {
106+
return &ReactionMetadata{}
107+
},
106108
UserLogin: func() any {
107109
return &UserLoginMetadata{}
108110
},

pkg/connector/reaction.go

Lines changed: 166 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ type linePaidReactionRef struct {
3636
Version int
3737
}
3838

39-
func (ref linePaidReactionRef) networkEmojiID() networkid.EmojiID {
40-
return networkid.EmojiID("paid:" + ref.ProductID + ":" + ref.EmojiID)
39+
type lineReactionRef struct {
40+
typ line.ReactionType
41+
}
42+
43+
type ReactionMetadata struct {
44+
MatrixKey string `json:"matrix_key,omitempty"`
45+
ReactionType line.ReactionType `json:"reaction_type"`
4146
}
4247

4348
func (ref linePaidReactionRef) reactionType() line.ReactionType {
@@ -51,6 +56,61 @@ func (ref linePaidReactionRef) reactionType() line.ReactionType {
5156
}
5257
}
5358

59+
func cloneLineReactionType(typ line.ReactionType) line.ReactionType {
60+
cloned := line.ReactionType{
61+
PredefinedReactionType: typ.PredefinedReactionType,
62+
}
63+
if typ.PaidReactionType != nil {
64+
paid := *typ.PaidReactionType
65+
cloned.PaidReactionType = &paid
66+
}
67+
return cloned
68+
}
69+
70+
func newLineReactionRef(typ line.ReactionType) (lineReactionRef, error) {
71+
hasPredefined := typ.PredefinedReactionType != 0
72+
hasPaid := typ.PaidReactionType != nil
73+
if hasPredefined == hasPaid {
74+
return lineReactionRef{}, errors.New("reaction type must contain exactly one predefined or paid reaction")
75+
}
76+
if hasPredefined {
77+
if _, ok := line.PredefinedReactionEmoji[typ.PredefinedReactionType]; !ok {
78+
return lineReactionRef{}, fmt.Errorf("unknown predefined reaction type %d", typ.PredefinedReactionType)
79+
}
80+
} else if typ.PaidReactionType.ProductID == "" || typ.PaidReactionType.EmojiID == "" {
81+
return lineReactionRef{}, errors.New("paid reaction is missing product or emoji ID")
82+
}
83+
return lineReactionRef{typ: cloneLineReactionType(typ)}, nil
84+
}
85+
86+
func (ref lineReactionRef) reactionType() line.ReactionType {
87+
return cloneLineReactionType(ref.typ)
88+
}
89+
90+
func (ref lineReactionRef) networkEmojiID() networkid.EmojiID {
91+
if ref.typ.PaidReactionType != nil {
92+
return networkid.EmojiID("paid:" + ref.typ.PaidReactionType.ProductID + ":" + ref.typ.PaidReactionType.EmojiID)
93+
}
94+
return networkid.EmojiID("predefined:" + strconv.Itoa(ref.typ.PredefinedReactionType))
95+
}
96+
97+
func (ref lineReactionRef) equal(other lineReactionRef) bool {
98+
if ref.typ.PredefinedReactionType != other.typ.PredefinedReactionType {
99+
return false
100+
}
101+
if ref.typ.PaidReactionType == nil || other.typ.PaidReactionType == nil {
102+
return ref.typ.PaidReactionType == nil && other.typ.PaidReactionType == nil
103+
}
104+
return *ref.typ.PaidReactionType == *other.typ.PaidReactionType
105+
}
106+
107+
func (ref lineReactionRef) metadata(matrixKey string) *ReactionMetadata {
108+
return &ReactionMetadata{
109+
MatrixKey: matrixKey,
110+
ReactionType: ref.reactionType(),
111+
}
112+
}
113+
54114
// These are the LINE emoji/sticon URLs from the issue's pack-based reaction
55115
// set. Add more entries here as more Matrix emoji -> LINE CDN URL mappings are
56116
// captured.
@@ -302,6 +362,36 @@ func (lc *LineClient) getPaidReactionMXC(ctx context.Context, prt *line.PaidReac
302362
return mxc, nil
303363
}
304364

365+
func (lc *LineClient) convertReaction(
366+
ctx context.Context,
367+
typ line.ReactionType,
368+
sender bridgev2.EventSender,
369+
timestamp time.Time,
370+
) (*bridgev2.BackfillReaction, error) {
371+
ref, err := newLineReactionRef(typ)
372+
if err != nil {
373+
return nil, err
374+
}
375+
376+
var mxc string
377+
if ref.typ.PaidReactionType != nil {
378+
mxc, err = lc.getPaidReactionMXC(ctx, ref.typ.PaidReactionType)
379+
} else {
380+
mxc, err = lc.getPredefinedReactionMXC(ctx, ref.typ.PredefinedReactionType)
381+
}
382+
if err != nil {
383+
return nil, err
384+
}
385+
386+
return &bridgev2.BackfillReaction{
387+
Timestamp: timestamp,
388+
Sender: sender,
389+
EmojiID: ref.networkEmojiID(),
390+
Emoji: mxc,
391+
DBMetadata: ref.metadata(mxc),
392+
}, nil
393+
}
394+
305395
func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Message) ([]*bridgev2.BackfillReaction, bool) {
306396
if msg == nil || msg.Reactions == nil {
307397
return nil, false
@@ -319,18 +409,16 @@ func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Mes
319409
continue
320410
}
321411

322-
var (
323-
mxc string
324-
err error
325-
)
326-
switch {
327-
case reaction.ReactionType.PaidReactionType != nil:
328-
mxc, err = lc.getPaidReactionMXC(ctx, reaction.ReactionType.PaidReactionType)
329-
case reaction.ReactionType.PredefinedReactionType != 0:
330-
mxc, err = lc.getPredefinedReactionMXC(ctx, reaction.ReactionType.PredefinedReactionType)
331-
default:
332-
err = errors.New("reaction type is missing")
412+
var timestamp time.Time
413+
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
414+
timestamp = time.UnixMilli(timestampMillis)
333415
}
416+
convertedReaction, err := lc.convertReaction(
417+
ctx,
418+
reaction.ReactionType,
419+
lc.eventSenderForMID(reaction.FromUserMID),
420+
timestamp,
421+
)
334422
if err != nil {
335423
complete = false
336424
lc.UserLogin.Bridge.Log.Warn().
@@ -340,16 +428,7 @@ func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Mes
340428
Msg("Skipping unsupported historical reaction")
341429
continue
342430
}
343-
344-
var timestamp time.Time
345-
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
346-
timestamp = time.UnixMilli(timestampMillis)
347-
}
348-
converted = append(converted, &bridgev2.BackfillReaction{
349-
Timestamp: timestamp,
350-
Sender: lc.eventSenderForMID(reaction.FromUserMID),
351-
Emoji: mxc,
352-
})
431+
converted = append(converted, convertedReaction)
353432
}
354433
return converted, complete
355434
}
@@ -457,6 +536,61 @@ func linePaidReactionForMatrixEmoji(key string) (linePaidReactionRef, bool) {
457536
return ref, true
458537
}
459538

539+
func storedLineReactionForMatrixKey(key string, reactions []*database.Reaction) (lineReactionRef, bool) {
540+
var (
541+
found lineReactionRef
542+
hasFound bool
543+
)
544+
for _, reaction := range reactions {
545+
meta, ok := reaction.Metadata.(*ReactionMetadata)
546+
if !ok || meta == nil || meta.MatrixKey != key {
547+
continue
548+
}
549+
ref, err := newLineReactionRef(meta.ReactionType)
550+
if err != nil || (reaction.EmojiID != "" && reaction.EmojiID != ref.networkEmojiID()) {
551+
continue
552+
}
553+
if hasFound && !found.equal(ref) {
554+
return lineReactionRef{}, false
555+
}
556+
found = ref
557+
hasFound = true
558+
}
559+
return found, hasFound
560+
}
561+
562+
func (lc *LineClient) resolveMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (lineReactionRef, error) {
563+
key := msg.Content.RelatesTo.GetAnnotationKey()
564+
if paidRef, ok := linePaidReactionForMatrixEmoji(key); ok {
565+
ref, err := newLineReactionRef(paidRef.reactionType())
566+
if err != nil {
567+
return lineReactionRef{}, err
568+
}
569+
return ref, nil
570+
}
571+
if !strings.HasPrefix(key, "mxc://") {
572+
return lineReactionRef{}, unsupportedMatrixReactionError(key)
573+
}
574+
if msg.TargetMessage == nil || msg.Portal == nil || msg.Portal.Bridge == nil || msg.Portal.Bridge.DB == nil {
575+
return lineReactionRef{}, errors.New("reaction target database context is missing")
576+
}
577+
578+
reactions, err := msg.Portal.Bridge.DB.Reaction.GetAllToMessagePart(
579+
ctx,
580+
msg.Portal.Receiver,
581+
msg.TargetMessage.ID,
582+
msg.TargetMessage.PartID,
583+
)
584+
if err != nil {
585+
return lineReactionRef{}, fmt.Errorf("get target message reactions: %w", err)
586+
}
587+
ref, ok := storedLineReactionForMatrixKey(key, reactions)
588+
if !ok {
589+
return lineReactionRef{}, unsupportedMatrixReactionError(key)
590+
}
591+
return ref, nil
592+
}
593+
460594
func unsupportedMatrixReactionError(key string) error {
461595
return bridgev2.WrapErrorInStatus(fmt.Errorf("LINE does not support Matrix reaction %q", key)).
462596
WithStatus(event.MessageStatusFail).
@@ -572,9 +706,9 @@ func (lc *LineClient) consumeSentReqSeq(reqSeq int) bool {
572706

573707
func (lc *LineClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
574708
key := msg.Content.RelatesTo.GetAnnotationKey()
575-
ref, ok := linePaidReactionForMatrixEmoji(key)
576-
if !ok {
577-
return bridgev2.MatrixReactionPreResponse{}, unsupportedMatrixReactionError(key)
709+
ref, err := lc.resolveMatrixReaction(ctx, msg)
710+
if err != nil {
711+
return bridgev2.MatrixReactionPreResponse{}, err
578712
}
579713
return bridgev2.MatrixReactionPreResponse{
580714
SenderID: makeUserID(string(lc.UserLogin.ID)),
@@ -586,9 +720,9 @@ func (lc *LineClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2
586720

587721
func (lc *LineClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
588722
key := msg.Content.RelatesTo.GetAnnotationKey()
589-
ref, ok := linePaidReactionForMatrixEmoji(key)
590-
if !ok {
591-
return nil, unsupportedMatrixReactionError(key)
723+
ref, err := lc.resolveMatrixReaction(ctx, msg)
724+
if err != nil {
725+
return nil, err
592726
}
593727
targetID, err := parseReactionTargetMessageID(msg.TargetMessage.ID)
594728
if err != nil {
@@ -610,8 +744,9 @@ func (lc *LineClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.Ma
610744
}
611745

612746
return &database.Reaction{
613-
EmojiID: ref.networkEmojiID(),
614-
Emoji: key,
747+
EmojiID: ref.networkEmojiID(),
748+
Emoji: key,
749+
Metadata: ref.metadata(key),
615750
}, nil
616751
}
617752

0 commit comments

Comments
 (0)