Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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()) {
continue
}
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