Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pkg/connector/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ type LineClient struct {
knownMemberChatMIDs map[string]struct{} // chatMid -> current member chats returned by getAllChatMids
reactionIconMXC map[int]string // predefinedReactionType -> cached MXC URI
paidReactionIconMXC map[string]string // LINE sticon URL -> cached MXC URI
recentReactions sync.Map // "msgID\x00emoji" -> struct{} to dedup concurrent 139/140 events
unblockBackfills sync.Map // chat MID -> *unblockBackfillState while unblock history restoration is active

wg sync.WaitGroup
Expand Down
10 changes: 6 additions & 4 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ func (lc *LineConnector) GetConfig() (example string, data any, upgrader configu

func (lc *LineConnector) GetDBMetaTypes() database.MetaTypes {
return database.MetaTypes{
Portal: nil,
Ghost: nil,
Message: nil,
Reaction: nil,
Portal: nil,
Ghost: nil,
Message: nil,
Reaction: func() any {
return &ReactionMetadata{}
},
UserLogin: func() any {
return &UserLoginMetadata{}
},
Expand Down
197 changes: 166 additions & 31 deletions pkg/connector/reaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ type linePaidReactionRef struct {
Version int
}

func (ref linePaidReactionRef) networkEmojiID() networkid.EmojiID {
return networkid.EmojiID("paid:" + ref.ProductID + ":" + ref.EmojiID)
type lineReactionRef struct {
typ line.ReactionType
}

type ReactionMetadata struct {
MatrixKey string `json:"matrix_key,omitempty"`
ReactionType line.ReactionType `json:"reaction_type"`
}

func (ref linePaidReactionRef) reactionType() line.ReactionType {
Expand All @@ -51,6 +56,61 @@ func (ref linePaidReactionRef) reactionType() line.ReactionType {
}
}

func cloneLineReactionType(typ line.ReactionType) line.ReactionType {
cloned := line.ReactionType{
PredefinedReactionType: typ.PredefinedReactionType,
}
if typ.PaidReactionType != nil {
paid := *typ.PaidReactionType
cloned.PaidReactionType = &paid
}
return cloned
}

func newLineReactionRef(typ line.ReactionType) (lineReactionRef, error) {
hasPredefined := typ.PredefinedReactionType != 0
hasPaid := typ.PaidReactionType != nil
if hasPredefined == hasPaid {
return lineReactionRef{}, errors.New("reaction type must contain exactly one predefined or paid reaction")
}
if hasPredefined {
if _, ok := line.PredefinedReactionEmoji[typ.PredefinedReactionType]; !ok {
return lineReactionRef{}, fmt.Errorf("unknown predefined reaction type %d", typ.PredefinedReactionType)
}
} else if typ.PaidReactionType.ProductID == "" || typ.PaidReactionType.EmojiID == "" {
return lineReactionRef{}, errors.New("paid reaction is missing product or emoji ID")
}
return lineReactionRef{typ: cloneLineReactionType(typ)}, nil
}

func (ref lineReactionRef) reactionType() line.ReactionType {
return cloneLineReactionType(ref.typ)
}

func (ref lineReactionRef) networkEmojiID() networkid.EmojiID {
if ref.typ.PaidReactionType != nil {
return networkid.EmojiID("paid:" + ref.typ.PaidReactionType.ProductID + ":" + ref.typ.PaidReactionType.EmojiID)
}
return networkid.EmojiID("predefined:" + strconv.Itoa(ref.typ.PredefinedReactionType))
}

func (ref lineReactionRef) equal(other lineReactionRef) bool {
if ref.typ.PredefinedReactionType != other.typ.PredefinedReactionType {
return false
}
if ref.typ.PaidReactionType == nil || other.typ.PaidReactionType == nil {
return ref.typ.PaidReactionType == nil && other.typ.PaidReactionType == nil
}
return *ref.typ.PaidReactionType == *other.typ.PaidReactionType
}

func (ref lineReactionRef) metadata(matrixKey string) *ReactionMetadata {
return &ReactionMetadata{
MatrixKey: matrixKey,
ReactionType: ref.reactionType(),
}
}

// These are the LINE emoji/sticon URLs from the issue's pack-based reaction
// set. Add more entries here as more Matrix emoji -> LINE CDN URL mappings are
// captured.
Expand Down Expand Up @@ -302,6 +362,36 @@ func (lc *LineClient) getPaidReactionMXC(ctx context.Context, prt *line.PaidReac
return mxc, nil
}

func (lc *LineClient) convertReaction(
ctx context.Context,
typ line.ReactionType,
sender bridgev2.EventSender,
timestamp time.Time,
) (*bridgev2.BackfillReaction, error) {
ref, err := newLineReactionRef(typ)
if err != nil {
return nil, err
}

var mxc string
if ref.typ.PaidReactionType != nil {
mxc, err = lc.getPaidReactionMXC(ctx, ref.typ.PaidReactionType)
} else {
mxc, err = lc.getPredefinedReactionMXC(ctx, ref.typ.PredefinedReactionType)
}
if err != nil {
return nil, err
}

return &bridgev2.BackfillReaction{
Timestamp: timestamp,
Sender: sender,
EmojiID: ref.networkEmojiID(),
Emoji: mxc,
DBMetadata: ref.metadata(mxc),
}, nil
}

func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Message) ([]*bridgev2.BackfillReaction, bool) {
if msg == nil || msg.Reactions == nil {
return nil, false
Expand All @@ -319,18 +409,16 @@ func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Mes
continue
}

var (
mxc string
err error
)
switch {
case reaction.ReactionType.PaidReactionType != nil:
mxc, err = lc.getPaidReactionMXC(ctx, reaction.ReactionType.PaidReactionType)
case reaction.ReactionType.PredefinedReactionType != 0:
mxc, err = lc.getPredefinedReactionMXC(ctx, reaction.ReactionType.PredefinedReactionType)
default:
err = errors.New("reaction type is missing")
var timestamp time.Time
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
timestamp = time.UnixMilli(timestampMillis)
}
convertedReaction, err := lc.convertReaction(
ctx,
reaction.ReactionType,
lc.eventSenderForMID(reaction.FromUserMID),
timestamp,
)
if err != nil {
complete = false
lc.UserLogin.Bridge.Log.Warn().
Expand All @@ -340,16 +428,7 @@ func (lc *LineClient) convertMessageReactions(ctx context.Context, msg *line.Mes
Msg("Skipping unsupported historical reaction")
continue
}

var timestamp time.Time
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
timestamp = time.UnixMilli(timestampMillis)
}
converted = append(converted, &bridgev2.BackfillReaction{
Timestamp: timestamp,
Sender: lc.eventSenderForMID(reaction.FromUserMID),
Emoji: mxc,
})
converted = append(converted, convertedReaction)
}
return converted, complete
}
Expand Down Expand Up @@ -457,6 +536,61 @@ func linePaidReactionForMatrixEmoji(key string) (linePaidReactionRef, bool) {
return ref, true
}

func storedLineReactionForMatrixKey(key string, reactions []*database.Reaction) (lineReactionRef, bool) {
var (
found lineReactionRef
hasFound bool
)
for _, reaction := range reactions {
meta, ok := reaction.Metadata.(*ReactionMetadata)
if !ok || meta == nil || meta.MatrixKey != key {
continue
}
ref, err := newLineReactionRef(meta.ReactionType)
if err != nil || (reaction.EmojiID != "" && reaction.EmojiID != ref.networkEmojiID()) {
return lineReactionRef{}, false
}
Comment thread
indent-zero[bot] marked this conversation as resolved.
if hasFound && !found.equal(ref) {
return lineReactionRef{}, false
}
found = ref
hasFound = true
}
return found, hasFound
}

func (lc *LineClient) resolveMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (lineReactionRef, error) {
key := msg.Content.RelatesTo.GetAnnotationKey()
if paidRef, ok := linePaidReactionForMatrixEmoji(key); ok {
ref, err := newLineReactionRef(paidRef.reactionType())
if err != nil {
return lineReactionRef{}, err
}
return ref, nil
}
if !strings.HasPrefix(key, "mxc://") {
return lineReactionRef{}, unsupportedMatrixReactionError(key)
}
if msg.TargetMessage == nil || msg.Portal == nil || msg.Portal.Bridge == nil || msg.Portal.Bridge.DB == nil {
return lineReactionRef{}, errors.New("reaction target database context is missing")
}

reactions, err := msg.Portal.Bridge.DB.Reaction.GetAllToMessagePart(
ctx,
msg.Portal.Receiver,
msg.TargetMessage.ID,
msg.TargetMessage.PartID,
)
if err != nil {
return lineReactionRef{}, fmt.Errorf("get target message reactions: %w", err)
}
ref, ok := storedLineReactionForMatrixKey(key, reactions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent: legacy EmojiID="" rows are unreachable via this lookup.

Rows persisted before this PR were written with EmojiID="" and Metadata=nil. The Reaction metadata factory now returns &ReactionMetadata{} on load, so those rows come back with an empty MatrixKey, fail the meta.MatrixKey != key filter in storedLineReactionForMatrixKey, and this call returns unsupportedMatrixReactionError.

Trigger: any user tries to re-react (from Matrix) to a paid reaction that was originally bridged before this PR shipped. It resolves itself once someone else reacts and a live 139/140 event creates a new stable-ID row (the HasAllReactions=true sweep then also redacts the legacy row), but until then the paid MXC in the picker looks broken.

Worth calling out in the changelog / considering a one-shot migration that populates metadata for existing rows from Emoji if it's an MXC URL that maps to a known LINE sticker.

if !ok {
return lineReactionRef{}, unsupportedMatrixReactionError(key)
}
return ref, nil
}

func unsupportedMatrixReactionError(key string) error {
return bridgev2.WrapErrorInStatus(fmt.Errorf("LINE does not support Matrix reaction %q", key)).
WithStatus(event.MessageStatusFail).
Expand Down Expand Up @@ -572,9 +706,9 @@ func (lc *LineClient) consumeSentReqSeq(reqSeq int) bool {

func (lc *LineClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
key := msg.Content.RelatesTo.GetAnnotationKey()
ref, ok := linePaidReactionForMatrixEmoji(key)
if !ok {
return bridgev2.MatrixReactionPreResponse{}, unsupportedMatrixReactionError(key)
ref, err := lc.resolveMatrixReaction(ctx, msg)
if err != nil {
return bridgev2.MatrixReactionPreResponse{}, err
}
return bridgev2.MatrixReactionPreResponse{
SenderID: makeUserID(string(lc.UserLogin.ID)),
Expand All @@ -586,9 +720,9 @@ func (lc *LineClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2

func (lc *LineClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
key := msg.Content.RelatesTo.GetAnnotationKey()
ref, ok := linePaidReactionForMatrixEmoji(key)
if !ok {
return nil, unsupportedMatrixReactionError(key)
ref, err := lc.resolveMatrixReaction(ctx, msg)
if err != nil {
return nil, err
}
targetID, err := parseReactionTargetMessageID(msg.TargetMessage.ID)
if err != nil {
Expand All @@ -610,8 +744,9 @@ func (lc *LineClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.Ma
}

return &database.Reaction{
EmojiID: ref.networkEmojiID(),
Emoji: key,
EmojiID: ref.networkEmojiID(),
Emoji: key,
Metadata: ref.metadata(key),
}, nil
}

Expand Down
Loading
Loading