From d2cad8c57ec07ff8b0bce06e5485eeb0071e2e1b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Aug 2025 00:44:50 +0300 Subject: [PATCH 1/6] format: add MarkdownMentionWithName helper --- format/markdown.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/format/markdown.go b/format/markdown.go index 3d9979b4..3b1c1f51 100644 --- a/format/markdown.go +++ b/format/markdown.go @@ -57,7 +57,11 @@ type uriAble interface { } func MarkdownMention(id uriAble) string { - return MarkdownLink(id.String(), id.URI().MatrixToURL()) + return MarkdownMentionWithName(id.String(), id) +} + +func MarkdownMentionWithName(name string, id uriAble) string { + return MarkdownLink(name, id.URI().MatrixToURL()) } func MarkdownLink(name string, url string) string { From 7e07700a69437cea25ee99dfd3e2f213bcd70f94 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Aug 2025 00:47:55 +0300 Subject: [PATCH 2/6] format: add MarkdownMentionRoomID helper --- format/markdown.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/format/markdown.go b/format/markdown.go index 3b1c1f51..77ced0dc 100644 --- a/format/markdown.go +++ b/format/markdown.go @@ -64,6 +64,13 @@ func MarkdownMentionWithName(name string, id uriAble) string { return MarkdownLink(name, id.URI().MatrixToURL()) } +func MarkdownMentionRoomID(name string, id id.RoomID, via ...string) string { + if name == "" { + name = id.String() + } + return MarkdownLink(name, id.URI(via...).MatrixToURL()) +} + func MarkdownLink(name string, url string) string { return fmt.Sprintf("[%s](%s)", EscapeMarkdown(name), EscapeMarkdown(url)) } From fa7c1ae2bcd716f29a823a4167ec6a0c9206a5d2 Mon Sep 17 00:00:00 2001 From: Brad Murray Date: Mon, 25 Aug 2025 08:03:13 -0400 Subject: [PATCH 3/6] crypto/sqlstore: add index to make finding megolm sessions to backup faster (#402) ``` 2025-08-24T22:23:19Z debug [MatrixBridgeV2] {"level":"warn","component":"matrix","component":"client_loop","subcomponent":"sync_key_backup_loop","rows":0,"duration_seconds":1.046191042,"method":"EndRows","query":"SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id=?1 AND session IS NOT NULL AND key_backup_version != ?2","time":"2025-08-24T22:23:19.22077Z","message":"Query took long"} ``` before: ``` sqlite> EXPLAIN SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id='@brad:beeper.com/CHNWOJWEUC' AND sessi addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 25 0 0 Start at 25 1 OpenRead 0 48 0 15 0 root=48 iDb=0; crypto_megolm_inbound_session 2 OpenRead 1 49 0 k(3,,,) 2 root=49 iDb=0; sqlite_autoindex_crypto_megolm_inbound_session_1 3 String8 0 1 0 @brad:beeper.com/CHNWOJWEUC 0 r[1]='@brad:beeper.com/CHNWOJWEUC' 4 SeekGE 1 24 1 1 0 key=r[1] 5 IdxGT 1 24 1 1 0 key=r[1] 6 DeferredSeek 1 0 0 0 Move 0 to 1.rowid if needed 7 Column 0 5 2 128 r[2]= cursor 0 column 5 8 IsNull 2 23 0 0 if r[2]==NULL goto 23 9 Column 0 14 2 0 r[2]=crypto_megolm_inbound_session.key_backup_version 10 Eq 3 23 2 BINARY-8 82 if r[2]==r[3] goto 23 11 Column 0 4 4 0 r[4]= cursor 0 column 4 12 Column 0 2 5 0 r[5]= cursor 0 column 2 13 Column 0 3 6 0 r[6]= cursor 0 column 3 14 Column 0 5 7 0 r[7]= cursor 0 column 5 15 Column 0 6 8 0 r[8]= cursor 0 column 6 16 Column 0 9 9 0 r[9]= cursor 0 column 9 17 Column 0 10 10 0 r[10]= cursor 0 column 10 18 Column 0 11 11 0 r[11]= cursor 0 column 11 19 Column 0 12 12 0 r[12]= cursor 0 column 12 20 Column 0 13 13 0 0 r[13]=crypto_megolm_inbound_session.is_scheduled 21 Column 0 14 14 0 r[14]=crypto_megolm_inbound_session.key_backup_version 22 ResultRow 4 11 0 0 output=r[4..14] 23 Next 1 5 0 0 24 Halt 0 0 0 0 25 Transaction 0 0 55 0 1 usesStmtJournal=0 26 Integer 1 3 0 0 r[3]=1 27 Goto 0 1 0 0 sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session ; +----------+ | COUNT(*) | +----------+ | 168792 | +----------+ sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session WHERE session IS NULL; +----------+ | COUNT(*) | +----------+ | 39 | +----------+ sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session WHERE key_backup_version != 1; +----------+ | COUNT(*) | +----------+ | 39 | +----------+ ``` after: ``` sqlite> CREATE INDEX idx_megolm_filtered ...> ON crypto_megolm_inbound_session(account_id, key_backup_version, session); sqlite> EXPLAIN SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id='@brad:beeper.com/CHNWOJWEUC' AND session IS NOT NULL AND key_backup_version != 1; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 25 0 0 Start at 25 1 OpenRead 0 48 0 15 0 root=48 iDb=0; crypto_megolm_inbound_session 2 OpenRead 1 91264 0 k(4,,,,) 2 root=91264 iDb=0; idx_megolm_filtered 3 String8 0 1 0 @brad:beeper.com/CHNWOJWEUC 0 r[1]='@brad:beeper.com/CHNWOJWEUC' 4 SeekGE 1 24 1 1 0 key=r[1] 5 IdxGT 1 24 1 1 0 key=r[1] 6 DeferredSeek 1 0 0 0 Move 0 to 1.rowid if needed 7 Column 1 2 2 128 r[2]= cursor 1 column 2 8 IsNull 2 23 0 0 if r[2]==NULL goto 23 9 Column 1 1 2 0 r[2]=crypto_megolm_inbound_session.key_backup_version 10 Eq 3 23 2 BINARY-8 82 if r[2]==r[3] goto 23 11 Column 0 4 4 0 r[4]= cursor 0 column 4 12 Column 0 2 5 0 r[5]= cursor 0 column 2 13 Column 0 3 6 0 r[6]= cursor 0 column 3 14 Column 1 2 7 0 r[7]= cursor 1 column 2 15 Column 0 6 8 0 r[8]= cursor 0 column 6 16 Column 0 9 9 0 r[9]= cursor 0 column 9 17 Column 0 10 10 0 r[10]= cursor 0 column 10 18 Column 0 11 11 0 r[11]= cursor 0 column 11 19 Column 0 12 12 0 r[12]= cursor 0 column 12 20 Column 0 13 13 0 0 r[13]=crypto_megolm_inbound_session.is_scheduled 21 Column 1 1 14 0 r[14]=crypto_megolm_inbound_session.key_backup_version 22 ResultRow 4 11 0 0 output=r[4..14] 23 Next 1 5 0 0 24 Halt 0 0 0 0 25 Transaction 0 0 56 0 1 usesStmtJournal=0 26 Integer 1 3 0 0 r[3]=1 27 Goto 0 1 0 0 sqlite> ``` --- crypto/sql_store_upgrade/00-latest-revision.sql | 4 +++- .../18-megolm-inbound-session-backup-index.sql | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 crypto/sql_store_upgrade/18-megolm-inbound-session-backup-index.sql diff --git a/crypto/sql_store_upgrade/00-latest-revision.sql b/crypto/sql_store_upgrade/00-latest-revision.sql index 00dd1387..af8ab5cc 100644 --- a/crypto/sql_store_upgrade/00-latest-revision.sql +++ b/crypto/sql_store_upgrade/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v17 (compatible with v15+): Latest revision +-- v0 -> v18 (compatible with v15+): Latest revision CREATE TABLE IF NOT EXISTS crypto_account ( account_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, @@ -73,6 +73,8 @@ CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session ( key_backup_version TEXT NOT NULL DEFAULT '', PRIMARY KEY (account_id, session_id) ); +-- Useful index to find keys that need backing up +CREATE INDEX crypto_megolm_inbound_session_backup_idx ON crypto_megolm_inbound_session(account_id, key_backup_version) WHERE session IS NOT NULL; CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session ( account_id TEXT, diff --git a/crypto/sql_store_upgrade/18-megolm-inbound-session-backup-index.sql b/crypto/sql_store_upgrade/18-megolm-inbound-session-backup-index.sql new file mode 100644 index 00000000..da26da0f --- /dev/null +++ b/crypto/sql_store_upgrade/18-megolm-inbound-session-backup-index.sql @@ -0,0 +1,2 @@ +-- v18 (compatible with v15+): Add an index to the megolm_inbound_session table to make finding sessions to backup faster +CREATE INDEX crypto_megolm_inbound_session_backup_idx ON crypto_megolm_inbound_session(account_id, key_backup_version) WHERE session IS NOT NULL; From c04d0b66819b5ca54425140c1bd77c100262a9c7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 25 Aug 2025 12:58:28 +0300 Subject: [PATCH 4/6] bridgev2: merge mentions and url previews when merging caption --- bridgev2/networkinterface.go | 4 ++++ event/message.go | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/bridgev2/networkinterface.go b/bridgev2/networkinterface.go index eb38bd2d..d792ed0d 100644 --- a/bridgev2/networkinterface.go +++ b/bridgev2/networkinterface.go @@ -117,11 +117,15 @@ func MergeCaption(textPart, mediaPart *ConvertedMessagePart) *ConvertedMessagePa mediaPart.Content.EnsureHasHTML() mediaPart.Content.Body += "\n\n" + textPart.Content.Body mediaPart.Content.FormattedBody += "

" + textPart.Content.FormattedBody + mediaPart.Content.Mentions = mediaPart.Content.Mentions.Merge(textPart.Content.Mentions) + mediaPart.Content.BeeperLinkPreviews = append(mediaPart.Content.BeeperLinkPreviews, textPart.Content.BeeperLinkPreviews...) } else { mediaPart.Content.FileName = mediaPart.Content.Body mediaPart.Content.Body = textPart.Content.Body mediaPart.Content.Format = textPart.Content.Format mediaPart.Content.FormattedBody = textPart.Content.FormattedBody + mediaPart.Content.Mentions = textPart.Content.Mentions + mediaPart.Content.BeeperLinkPreviews = textPart.Content.BeeperLinkPreviews } if metaMerger, ok := mediaPart.DBMetadata.(database.MetaMerger); ok { metaMerger.CopyFrom(textPart.DBMetadata) diff --git a/event/message.go b/event/message.go index f16822f2..cc7c8261 100644 --- a/event/message.go +++ b/event/message.go @@ -273,6 +273,18 @@ func (m *Mentions) Has(userID id.UserID) bool { return m != nil && slices.Contains(m.UserIDs, userID) } +func (m *Mentions) Merge(other *Mentions) *Mentions { + if m == nil { + return other + } else if other == nil { + return m + } + return &Mentions{ + UserIDs: slices.Concat(m.UserIDs, other.UserIDs), + Room: m.Room || other.Room, + } +} + type EncryptedFileInfo struct { attachment.EncryptedFile URL id.ContentURIString `json:"url"` From 0fab92dbc1cafb65688d02273c3553c01cd0dc4f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 25 Aug 2025 12:58:40 +0300 Subject: [PATCH 5/6] event: add third party invite state event content --- event/content.go | 1 + event/member.go | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/event/content.go b/event/content.go index 779330af..5924ffe3 100644 --- a/event/content.go +++ b/event/content.go @@ -18,6 +18,7 @@ import ( // This is used by Content.ParseRaw() for creating the correct type of struct. var TypeMap = map[Type]reflect.Type{ StateMember: reflect.TypeOf(MemberEventContent{}), + StateThirdPartyInvite: reflect.TypeOf(ThirdPartyInviteEventContent{}), StatePowerLevels: reflect.TypeOf(PowerLevelsEventContent{}), StateCanonicalAlias: reflect.TypeOf(CanonicalAliasEventContent{}), StateRoomName: reflect.TypeOf(RoomNameEventContent{}), diff --git a/event/member.go b/event/member.go index 3e53893a..9956a36b 100644 --- a/event/member.go +++ b/event/member.go @@ -7,8 +7,6 @@ package event import ( - "encoding/json" - "maunium.net/go/mautrix/id" ) @@ -47,11 +45,25 @@ type MemberEventContent struct { MSC4293RedactEvents bool `json:"org.matrix.msc4293.redact_events,omitempty"` } -type ThirdPartyInvite struct { - DisplayName string `json:"display_name"` - Signed struct { - Token string `json:"token"` - Signatures json.RawMessage `json:"signatures"` - MXID string `json:"mxid"` - } `json:"signed"` +type SignedThirdPartyInvite struct { + Token string `json:"token"` + Signatures map[string]map[id.KeyID]string `json:"signatures,omitempty"` + MXID string `json:"mxid"` +} + +type ThirdPartyInvite struct { + DisplayName string `json:"display_name"` + Signed SignedThirdPartyInvite `json:"signed"` +} + +type ThirdPartyInviteEventContent struct { + DisplayName string `json:"display_name"` + KeyValidityURL string `json:"key_validity_url"` + PublicKey id.Ed25519 `json:"public_key"` + PublicKeys []ThirdPartyInviteKey `json:"public_keys,omitempty"` +} + +type ThirdPartyInviteKey struct { + KeyValidityURL string `json:"key_validity_url,omitempty"` + PublicKey id.Ed25519 `json:"public_key"` } From 5ac8a888a3a5b11165172a4bab0e65c74ade1737 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 25 Aug 2025 17:15:19 +0300 Subject: [PATCH 6/6] bridgev2/portal: make UpdateDisappearingSetting more versatile --- bridgev2/portal.go | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/bridgev2/portal.go b/bridgev2/portal.go index 7c3a56c2..0aae674d 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -4038,7 +4038,15 @@ func DisappearingMessageNotice(expiration time.Duration, implicit bool) *event.M return content } -func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting database.DisappearingSetting, sender MatrixAPI, ts time.Time, implicit, save bool) bool { +type UpdateDisappearingSettingOpts struct { + Sender MatrixAPI + Timestamp time.Time + Implicit bool + Save bool + SendNotice bool +} + +func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting database.DisappearingSetting, opts UpdateDisappearingSettingOpts) bool { if setting.Timer == 0 { setting.Type = event.DisappearingTypeNone } @@ -4047,7 +4055,7 @@ func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting dat } portal.Disappear.Type = setting.Type portal.Disappear.Timer = setting.Timer - if save { + if opts.Save { err := portal.Save(ctx) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to save portal to database after updating disappearing setting") @@ -4057,21 +4065,21 @@ func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting dat return true } - portal.sendRoomMeta(ctx, sender, ts, event.StateBeeperDisappearingTimer, "", setting.ToEventContent()) - - content := DisappearingMessageNotice(setting.Timer, implicit) - if sender == nil { - sender = portal.Bridge.Bot + if opts.Sender == nil { + opts.Sender = portal.Bridge.Bot } - _, err := sender.SendMessage(ctx, portal.MXID, event.EventMessage, &event.Content{ + portal.sendRoomMeta(ctx, opts.Sender, opts.Timestamp, event.StateBeeperDisappearingTimer, "", setting.ToEventContent()) + + content := DisappearingMessageNotice(setting.Timer, opts.Implicit) + _, err := opts.Sender.SendMessage(ctx, portal.MXID, event.EventMessage, &event.Content{ Parsed: content, - }, &MatrixSendExtra{Timestamp: ts}) + }, &MatrixSendExtra{Timestamp: opts.Timestamp}) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to send disappearing messages notice") } else { zerolog.Ctx(ctx).Debug(). Dur("new_timer", portal.Disappear.Timer). - Bool("implicit", implicit). + Bool("implicit", opts.Implicit). Msg("Sent disappearing messages notice") } return true @@ -4162,7 +4170,13 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *ChatInfo, source *Us changed = portal.updateAvatar(ctx, info.Avatar, sender, ts) || changed } if info.Disappear != nil { - changed = portal.UpdateDisappearingSetting(ctx, *info.Disappear, sender, ts, false, false) || changed + changed = portal.UpdateDisappearingSetting(ctx, *info.Disappear, UpdateDisappearingSettingOpts{ + Sender: sender, + Timestamp: ts, + Implicit: false, + Save: false, + SendNotice: true, + }) || changed } if info.ParentID != nil { changed = portal.updateParent(ctx, *info.ParentID, source) || changed