From f508e3156f39fab353bc2962551d366e04d582c7 Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:00:37 -0500 Subject: [PATCH 01/16] started on xmpp's XEP-0461 Message Replies implementation, mostly grabbed from prior art on kousu's fork, this is still buggy tho --- bridge/xmpp/xmpp.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 6a291c5ad..bc9aa407a 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -8,6 +8,7 @@ import ( "sync" "time" + lru "github.com/hashicorp/golang-lru" "github.com/jpillora/backoff" "github.com/matterbridge-org/matterbridge/bridge" "github.com/matterbridge-org/matterbridge/bridge/config" @@ -33,6 +34,7 @@ type Bxmpp struct { xc *xmpp.Client xmppMap map[string]string connected bool + cache *lru.Cache sync.RWMutex avatarAvailability map[string]bool @@ -58,12 +60,18 @@ type Bxmpp struct { } func New(cfg *bridge.Config) bridge.Bridger { + newCache, err := lru.New(5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache: %v", err) + } + return &Bxmpp{ Config: cfg, xmppMap: make(map[string]string), avatarAvailability: make(map[string]bool), avatarMap: make(map[string]string), httpUploadBuffer: make(map[string]*UploadBufferEntry), + cache: newCache, } } @@ -142,16 +150,27 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } } + // XEP-0461: populate reply fields if this message is a reply. + var replyID, replyTo string + if msg.ParentValid() { + if stanzaID, ok := b.cache.Get(msg.ParentID); ok { + replyID = stanzaID.(string) + } + replyTo = msg.Channel + "@" + b.GetString("Muc") + "/" + b.GetString("Nick") + } + // Post normal message. b.Log.Debugf("=> Sending message %#v", msg) if _, err := b.xc.Send(xmpp.Chat{ - Type: "groupchat", - Remote: msg.Channel + "@" + b.GetString("Muc"), - Text: msg.Username + msg.Text, + Type: "groupchat", + Remote: msg.Channel + "@" + b.GetString("Muc"), + Text: msg.Username + msg.Text, + ID: msg.ID, + ReplyID: replyID, + ReplyTo: replyTo, }); err != nil { return "", err } - // Generate a dummy ID because to avoid collision with other internal messages // However this does not provide proper Edits/Replies integration on XMPP side. msgID := xid.New().String() @@ -300,6 +319,11 @@ func (b *Bxmpp) handleXMPP() error { if v.Type == "groupchat" { b.Log.Debugf("== Receiving %#v", v) + // XEP-0461: Cache StanzaID to use for Message Replies + if v.StanzaID.ID != "" { + b.cache.Add(v.ID, v.StanzaID.ID) + } + // Skip invalid messages. if b.skipMessage(v) { continue From 25ecf4f169cdb62d606abb5a309a20e070f6c5be Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:35:21 -0500 Subject: [PATCH 02/16] add msg.ID --- bridge/xmpp/xmpp.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index bc9aa407a..3f7ff95a6 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -161,19 +161,18 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // Post normal message. b.Log.Debugf("=> Sending message %#v", msg) + msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text, - ID: msg.ID, + ID: msgID, ReplyID: replyID, ReplyTo: replyTo, }); err != nil { return "", err } // Generate a dummy ID because to avoid collision with other internal messages - // However this does not provide proper Edits/Replies integration on XMPP side. - msgID := xid.New().String() return msgID, nil } From 458b838c8a4a0d9b3abff5607c25ad1135a20a5e Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:50:02 -0500 Subject: [PATCH 03/16] cache stanza-id id and the original author's JID for replies --- bridge/xmpp/xmpp.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 3f7ff95a6..0ebdd2ffb 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -153,10 +153,11 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // XEP-0461: populate reply fields if this message is a reply. var replyID, replyTo string if msg.ParentValid() { - if stanzaID, ok := b.cache.Get(msg.ParentID); ok { - replyID = stanzaID.(string) + if info, ok := b.cache.Get(msg.ParentID); ok { + si := info.(stanzaInfo) + replyID = si.stanzaID + replyTo = si.from } - replyTo = msg.Channel + "@" + b.GetString("Muc") + "/" + b.GetString("Nick") } // Post normal message. @@ -289,6 +290,11 @@ func (b *Bxmpp) xmppKeepAlive() chan bool { return done } +type stanzaInfo struct { + stanzaID string + from string +} + func (b *Bxmpp) handleXMPP() error { b.startTime = time.Now() @@ -320,7 +326,7 @@ func (b *Bxmpp) handleXMPP() error { // XEP-0461: Cache StanzaID to use for Message Replies if v.StanzaID.ID != "" { - b.cache.Add(v.ID, v.StanzaID.ID) + b.cache.Add(v.ID, stanzaInfo{stanzaID: v.StanzaID.ID, from: v.Remote}) } // Skip invalid messages. From 1ead0dbce9ac73d7a572ed860552e3333e970bbc Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 15:28:17 -0500 Subject: [PATCH 04/16] xmpp: return to using IDs as matterbridge IDs. Previously, the xmpp bridge tried to be helpful by storing the XEP-0359 StanzaID as the matterbridge ID; it's logical but subtly broke XEP-0461 replies when actually implemented: messages _received_ from XMPP would have a StanzaID and could be looked up when any other bridge replied to them, but messages _sent_ from other bridges would not know their StanzaID and an attempt to reply to them would just get lost. It is simpler to stick with tracking the internal mapping between messages using the internally generated message IDs, and instead keep StanzaIDs in a cache private to xmpp. Plus, the old code that assumed they existed without checking, and that is wrong; StanzaIDs are an optional XEP, just a very common one. If this code ever ran into a bare-bones server without them it would have completely lost track of all XMPP messages. --- bridge/xmpp/xmpp.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 0ebdd2ffb..02c551273 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -324,8 +324,9 @@ func (b *Bxmpp) handleXMPP() error { if v.Type == "groupchat" { b.Log.Debugf("== Receiving %#v", v) - // XEP-0461: Cache StanzaID to use for Message Replies if v.StanzaID.ID != "" { + // Here the stanza-id has been set by the server and can be used to provide replies + // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id b.cache.Add(v.ID, stanzaInfo{stanzaID: v.StanzaID.ID, from: v.Remote}) } @@ -356,9 +357,7 @@ func (b *Bxmpp) handleXMPP() error { Account: b.Account, Avatar: avatar, UserID: v.Remote, - // Here the stanza-id has been set by the server and can be used to provide replies - // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id - ID: v.StanzaID.ID, + ID: v.ID, Event: event, Extra: make(map[string][]any), } From f163fa9401bc353ca92640978e408083e38ee0f3 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:10:29 -0500 Subject: [PATCH 05/16] xmpp: replies: stanzaInfo -> replyInfo and cache -> replyHeaders I think this is more understandable? I was hoping to maybe generate the xmpp.clientReply directly but that is a private struct --- bridge/xmpp/xmpp.go | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 02c551273..9b8d80984 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -30,11 +30,11 @@ type UploadBufferEntry struct { type Bxmpp struct { *bridge.Config - startTime time.Time - xc *xmpp.Client - xmppMap map[string]string - connected bool - cache *lru.Cache + startTime time.Time + xc *xmpp.Client + xmppMap map[string]string + connected bool + replyHeaders *lru.Cache sync.RWMutex avatarAvailability map[string]bool @@ -60,7 +60,7 @@ type Bxmpp struct { } func New(cfg *bridge.Config) bridge.Bridger { - newCache, err := lru.New(5000) + replyHeaders, err := lru.New(5000) if err != nil { cfg.Log.Fatalf("Could not create LRU cache: %v", err) } @@ -71,7 +71,7 @@ func New(cfg *bridge.Config) bridge.Bridger { avatarAvailability: make(map[string]bool), avatarMap: make(map[string]string), httpUploadBuffer: make(map[string]*UploadBufferEntry), - cache: newCache, + replyHeaders: replyHeaders, } } @@ -151,12 +151,10 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } // XEP-0461: populate reply fields if this message is a reply. - var replyID, replyTo string + var reply replyInfo if msg.ParentValid() { - if info, ok := b.cache.Get(msg.ParentID); ok { - si := info.(stanzaInfo) - replyID = si.stanzaID - replyTo = si.from + if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { + reply = _reply.(replyInfo) } } @@ -168,8 +166,8 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text, ID: msgID, - ReplyID: replyID, - ReplyTo: replyTo, + ReplyID: reply.ID, + ReplyTo: reply.To, }); err != nil { return "", err } @@ -290,9 +288,9 @@ func (b *Bxmpp) xmppKeepAlive() chan bool { return done } -type stanzaInfo struct { - stanzaID string - from string +type replyInfo struct { + ID string + To string } func (b *Bxmpp) handleXMPP() error { @@ -327,7 +325,7 @@ func (b *Bxmpp) handleXMPP() error { if v.StanzaID.ID != "" { // Here the stanza-id has been set by the server and can be used to provide replies // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id - b.cache.Add(v.ID, stanzaInfo{stanzaID: v.StanzaID.ID, from: v.Remote}) + b.replyHeaders.Add(v.ID, replyInfo{ID: v.StanzaID.ID, To: v.Remote}) } // Skip invalid messages. From 1c2db980837afba417b213bf4acef8769950014c Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:12:19 -0500 Subject: [PATCH 06/16] fmt --- bridge/xmpp/xmpp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 9b8d80984..18c2d3d2b 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -355,9 +355,9 @@ func (b *Bxmpp) handleXMPP() error { Account: b.Account, Avatar: avatar, UserID: v.Remote, - ID: v.ID, - Event: event, - Extra: make(map[string][]any), + ID: v.ID, + Event: event, + Extra: make(map[string][]any), } // Check if we have an action event. From 82fb5a7b2d4af93dd179cad0c5464f686941417e Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:18:24 -0500 Subject: [PATCH 07/16] Replace replyInfo with xmpp.Reply (formerly the private xmpp.clientReply, now public) This was redundant. We might as well just share the struct between the two modules. --- bridge/xmpp/xmpp.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 18c2d3d2b..9a75d7e1f 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -151,10 +151,10 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } // XEP-0461: populate reply fields if this message is a reply. - var reply replyInfo + var reply xmpp.Reply if msg.ParentValid() { if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { - reply = _reply.(replyInfo) + reply = _reply.(xmpp.Reply) } } @@ -162,12 +162,11 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { b.Log.Debugf("=> Sending message %#v", msg) msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ - Type: "groupchat", - Remote: msg.Channel + "@" + b.GetString("Muc"), - Text: msg.Username + msg.Text, - ID: msgID, - ReplyID: reply.ID, - ReplyTo: reply.To, + Type: "groupchat", + Remote: msg.Channel + "@" + b.GetString("Muc"), + Text: msg.Username + msg.Text, + ID: msgID, + Reply: reply, }); err != nil { return "", err } @@ -288,11 +287,6 @@ func (b *Bxmpp) xmppKeepAlive() chan bool { return done } -type replyInfo struct { - ID string - To string -} - func (b *Bxmpp) handleXMPP() error { b.startTime = time.Now() @@ -325,7 +319,7 @@ func (b *Bxmpp) handleXMPP() error { if v.StanzaID.ID != "" { // Here the stanza-id has been set by the server and can be used to provide replies // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id - b.replyHeaders.Add(v.ID, replyInfo{ID: v.StanzaID.ID, To: v.Remote}) + b.replyHeaders.Add(v.ID, xmpp.Reply{ID: v.StanzaID.ID, To: v.Remote}) } // Skip invalid messages. From 66df18859ce9e163e430f824997393f8c21e2374 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:30:05 -0500 Subject: [PATCH 08/16] Comment --- bridge/xmpp/xmpp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 9a75d7e1f..2c8165944 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -160,6 +160,7 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // Post normal message. b.Log.Debugf("=> Sending message %#v", msg) + // Generate a dummy ID because to avoid collision with other internal messages msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ Type: "groupchat", @@ -170,7 +171,6 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { }); err != nil { return "", err } - // Generate a dummy ID because to avoid collision with other internal messages return msgID, nil } From 1f577c548e7473c4d6714485abd2e17d7fd9e0b4 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 17:07:34 -0500 Subject: [PATCH 09/16] XMPP XEP-0461 message replies when the reply comes from the XMPP side. This adds 'xmpp.KeepQuotedReply' config option that needs to be documented. I took the code from the matrix bridge; it's buggy if there are multiple. I would prefer not to add that option at all; the destination bridge should be able to figure out if the quote should be stripped, based on whether it supports replies on its side. This also adds a second lru.Cache. I feel like maybe there's a way to avoid that, but maybe there's not. I would like some feedback on that. --- bridge/xmpp/xmpp.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 2c8165944..d1387ac5c 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -34,6 +34,7 @@ type Bxmpp struct { xc *xmpp.Client xmppMap map[string]string connected bool + stanzaIDs *lru.Cache replyHeaders *lru.Cache sync.RWMutex @@ -60,6 +61,10 @@ type Bxmpp struct { } func New(cfg *bridge.Config) bridge.Bridger { + stanzaIDs, err := lru.New(5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache: %v", err) + } replyHeaders, err := lru.New(5000) if err != nil { cfg.Log.Fatalf("Could not create LRU cache: %v", err) @@ -71,6 +76,7 @@ func New(cfg *bridge.Config) bridge.Bridger { avatarAvailability: make(map[string]bool), avatarMap: make(map[string]string), httpUploadBuffer: make(map[string]*UploadBufferEntry), + stanzaIDs: stanzaIDs, replyHeaders: replyHeaders, } } @@ -319,6 +325,7 @@ func (b *Bxmpp) handleXMPP() error { if v.StanzaID.ID != "" { // Here the stanza-id has been set by the server and can be used to provide replies // as explained in XEP-0461 https://xmpp.org/extensions/xep-0461.html#business-id + b.stanzaIDs.Add(v.StanzaID.ID, v.ID) b.replyHeaders.Add(v.ID, xmpp.Reply{ID: v.StanzaID.ID, To: v.Remote}) } @@ -342,6 +349,28 @@ func (b *Bxmpp) handleXMPP() error { avatar = getAvatar(b.avatarMap, v.Remote, b.General) } + // If there was a , map the StanzaID to the local matterbridge message ID + // so we can inform the other bridges of this message has a parent + var parentID string + if v.Reply.ID != `` { + if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { + parentID = _parentID.(string) + } + + body := v.Text + if !b.GetBool("keepquotedreply") { + for strings.HasPrefix(body, "> ") { + lineIdx := strings.IndexRune(body, '\n') + if lineIdx == -1 { + body = "" + } else { + body = body[(lineIdx + 1):] + } + } + } + v.Text = body + } + rmsg := config.Message{ Username: b.parseNick(v.Remote), Text: v.Text, @@ -352,6 +381,7 @@ func (b *Bxmpp) handleXMPP() error { ID: v.ID, Event: event, Extra: make(map[string][]any), + ParentID: parentID, } // Check if we have an action event. From db541dd6b27b9f6a8f0c6afbd93da9eac0ea8cee Mon Sep 17 00:00:00 2001 From: sh4sh <6833405+sh4sh@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:36:01 -0500 Subject: [PATCH 10/16] add parentText to config.Message for destination bridges to use when formatting replies/quotes --- bridge/config/config.go | 27 ++++++++++++++------------- bridge/xmpp/xmpp.go | 41 ++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/bridge/config/config.go b/bridge/config/config.go index 994b83488..80770df70 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -35,19 +35,20 @@ const ( const ParentIDNotFound = "msg-parent-not-found" type Message struct { - Text string `json:"text"` - Channel string `json:"channel"` - Username string `json:"username"` - UserID string `json:"userid"` // userid on the bridge - Avatar string `json:"avatar"` - Account string `json:"account"` - Event string `json:"event"` - Protocol string `json:"protocol"` - Gateway string `json:"gateway"` - ParentID string `json:"parent_id"` - Timestamp time.Time `json:"timestamp"` - ID string `json:"id"` - Extra map[string][]interface{} + Text string `json:"text"` + Channel string `json:"channel"` + Username string `json:"username"` + UserID string `json:"userid"` // userid on the bridge + Avatar string `json:"avatar"` + Account string `json:"account"` + Event string `json:"event"` + Protocol string `json:"protocol"` + Gateway string `json:"gateway"` + ParentID string `json:"parent_id"` + ParentText string `json:"parent_text"` + Timestamp time.Time `json:"timestamp"` + ID string `json:"id"` + Extra map[string][]interface{} } func (m Message) ParentNotFound() bool { diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index d1387ac5c..c5b441698 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -352,36 +352,39 @@ func (b *Bxmpp) handleXMPP() error { // If there was a , map the StanzaID to the local matterbridge message ID // so we can inform the other bridges of this message has a parent var parentID string + var parentText string if v.Reply.ID != `` { if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { parentID = _parentID.(string) } - body := v.Text - if !b.GetBool("keepquotedreply") { - for strings.HasPrefix(body, "> ") { - lineIdx := strings.IndexRune(body, '\n') - if lineIdx == -1 { - body = "" - } else { - body = body[(lineIdx + 1):] - } + // Capture quoted lines into parentText so destination bridges can decide + // how they should be displayed. + for strings.HasPrefix(body, "> ") { + lineIdx := strings.IndexRune(body, '\n') + if lineIdx == -1 { + parentText += body[2:] + body = "" + } else { + parentText += body[2:lineIdx] + "\n" + body = body[(lineIdx + 1):] } } + parentText = strings.TrimRight(parentText, "\n") v.Text = body } rmsg := config.Message{ - Username: b.parseNick(v.Remote), - Text: v.Text, - Channel: b.parseChannel(v.Remote), - Account: b.Account, - Avatar: avatar, - UserID: v.Remote, - ID: v.ID, - Event: event, - Extra: make(map[string][]any), - ParentID: parentID, + Username: b.parseNick(v.Remote), + Text: v.Text, + Channel: b.parseChannel(v.Remote), + Account: b.Account, + Avatar: avatar, + UserID: v.Remote, + ID: v.ID, + Event: event, + ParentID: parentID, + ParentText: parentText, } // Check if we have an action event. From 25eef98beb338c30dd050ee5ba2519cfd0a3f5ac Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:30:17 -0500 Subject: [PATCH 11/16] Make Reply nullable. is optional, so it should be optional in our datamodel too. --- bridge/xmpp/xmpp.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index c5b441698..5817f7dbc 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -157,10 +157,11 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } // XEP-0461: populate reply fields if this message is a reply. - var reply xmpp.Reply + var reply *xmpp.Reply if msg.ParentValid() { if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { - reply = _reply.(xmpp.Reply) + _reply := _reply.(xmpp.Reply) + reply = &_reply } } @@ -353,7 +354,7 @@ func (b *Bxmpp) handleXMPP() error { // so we can inform the other bridges of this message has a parent var parentID string var parentText string - if v.Reply.ID != `` { + if v.Reply != nil { if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { parentID = _parentID.(string) } From 5a0bb1c2a0c55899a31040ab10c01838a9d8c943 Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:21:36 -0500 Subject: [PATCH 12/16] xmpp.Chat.ID -> xmpp.Chat.OriginID I think .ID is a better model of the underlying XMPP data structure but go-xmpp already defined OriginID and in practice ID == OriginID so using it is a less invasive change. --- bridge/xmpp/xmpp.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 5817f7dbc..b2dca4f2e 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -170,11 +170,11 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // Generate a dummy ID because to avoid collision with other internal messages msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ - Type: "groupchat", - Remote: msg.Channel + "@" + b.GetString("Muc"), - Text: msg.Username + msg.Text, - ID: msgID, - Reply: reply, + Type: "groupchat", + Remote: msg.Channel + "@" + b.GetString("Muc"), + Text: msg.Username + msg.Text, + OriginID: msgID, + Reply: reply, }); err != nil { return "", err } From 7ffb61a7672262ef04a7ce926f4ac3cbcf6cfb22 Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 23:34:47 -0500 Subject: [PATCH 13/16] Use typed lru --- bridge/xmpp/xmpp.go | 13 ++++++------- go.mod | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index b2dca4f2e..2e8ebdfe6 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -8,7 +8,7 @@ import ( "sync" "time" - lru "github.com/hashicorp/golang-lru" + lru "github.com/hashicorp/golang-lru/v2" "github.com/jpillora/backoff" "github.com/matterbridge-org/matterbridge/bridge" "github.com/matterbridge-org/matterbridge/bridge/config" @@ -34,8 +34,8 @@ type Bxmpp struct { xc *xmpp.Client xmppMap map[string]string connected bool - stanzaIDs *lru.Cache - replyHeaders *lru.Cache + stanzaIDs *lru.Cache[string, string] + replyHeaders *lru.Cache[string, xmpp.Reply] sync.RWMutex avatarAvailability map[string]bool @@ -61,11 +61,11 @@ type Bxmpp struct { } func New(cfg *bridge.Config) bridge.Bridger { - stanzaIDs, err := lru.New(5000) + stanzaIDs, err := lru.New[string, string](5000) if err != nil { cfg.Log.Fatalf("Could not create LRU cache: %v", err) } - replyHeaders, err := lru.New(5000) + replyHeaders, err := lru.New[string, xmpp.Reply](5000) if err != nil { cfg.Log.Fatalf("Could not create LRU cache: %v", err) } @@ -160,7 +160,6 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { var reply *xmpp.Reply if msg.ParentValid() { if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { - _reply := _reply.(xmpp.Reply) reply = &_reply } } @@ -356,7 +355,7 @@ func (b *Bxmpp) handleXMPP() error { var parentText string if v.Reply != nil { if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { - parentID = _parentID.(string) + parentID = _parentID } body := v.Text // Capture quoted lines into parentText so destination bridges can decide diff --git a/go.mod b/go.mod index 9d0fd08eb..50653ed31 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/gops v0.3.27 github.com/gorilla/schema v1.4.1 github.com/hashicorp/golang-lru v1.0.2 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jpillora/backoff v1.0.0 github.com/kyokomi/emoji/v2 v2.2.13 github.com/labstack/echo/v4 v4.12.0 @@ -76,7 +77,6 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect From e4738f710be4ecaa1d66b631b53d7d9037001929 Mon Sep 17 00:00:00 2001 From: kousu Date: Fri, 6 Mar 2026 01:04:46 -0500 Subject: [PATCH 14/16] Temporarily pin to edited dependency To be removed upon completion of https://github.com/xmppo/go-xmpp/pull/226 --- go.mod | 14 ++++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 50653ed31..43952f583 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( go.mau.fi/whatsmeow v0.0.0-20260123132415-83db04703aee golang.org/x/image v0.19.0 golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.33.0 + golang.org/x/text v0.34.0 gomod.garykim.dev/nc-talk v0.3.0 google.golang.org/protobuf v1.36.11 layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 @@ -133,11 +133,11 @@ require ( go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/util v0.9.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect google.golang.org/grpc v1.65.0 // indirect @@ -156,4 +156,6 @@ require ( //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 -go 1.24.0 +go 1.25.0 + +replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9 diff --git a/go.sum b/go.sum index ce4611f49..8f847efe6 100644 --- a/go.sum +++ b/go.sum @@ -311,6 +311,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9 h1:i3Wc7UxeqhfoipWxigPqO1JERwd1Dq76zqm9KtgR+Ds= +github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9/go.mod h1:veSQsIhh/ySAtFYcNwaH+qOTtbJaH3gWOLnlK1f8pRs= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0= @@ -419,8 +421,6 @@ github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6Fk github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xmppo/go-xmpp v0.3.2 h1:RAQf7yD2XNH1UAy4OPlofnI2qLVx1iQzAR7ghptvaA8= -github.com/xmppo/go-xmpp v0.3.2/go.mod h1:EBLbzPt4Y9OJBEF58Lhc4IrTnO226aIkbfosQo6KWeA= github.com/yaegashi/msgraph.go v0.1.4 h1:leDXSczAbwBpYFSmmZrdByTiPoUw8dbTfNMetAjJvbw= github.com/yaegashi/msgraph.go v0.1.4/go.mod h1:vgeYhHa5skJt/3lTyjGXThTZhwbhRnGo6uUxzoJIGME= github.com/yaegashi/wtz.go v0.0.2/go.mod h1:nOLA5QXsmdkRxBkP5tljhua13ADHCKirLBrzPf4PEJc= @@ -447,8 +447,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= @@ -476,8 +476,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -517,20 +517,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= From f8cb9ab87a8c5b07526fae8db634034149d78b5d Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:21:36 -0500 Subject: [PATCH 15/16] xmpp: replies: depend on tags to split quote from message. Most of this change is in the corresponding commit in go-xmpp. --- bridge/xmpp/xmpp.go | 18 +++--------------- go.mod | 3 ++- go.sum | 6 ++++-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 2e8ebdfe6..950950b9d 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -357,21 +357,9 @@ func (b *Bxmpp) handleXMPP() error { if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok { parentID = _parentID } - body := v.Text - // Capture quoted lines into parentText so destination bridges can decide - // how they should be displayed. - for strings.HasPrefix(body, "> ") { - lineIdx := strings.IndexRune(body, '\n') - if lineIdx == -1 { - parentText += body[2:] - body = "" - } else { - parentText += body[2:lineIdx] + "\n" - body = body[(lineIdx + 1):] - } - } - parentText = strings.TrimRight(parentText, "\n") - v.Text = body + + // TODO: ignore XMPP quote and rely on a cross-bridge message cache (see https://github.com/matterbridge-org/matterbridge/issues/142) + parentText = v.Reply.Quote } rmsg := config.Message{ diff --git a/go.mod b/go.mod index 43952f583..1d457d436 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect + github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5 // indirect github.com/Jeffail/gabs v1.4.0 // indirect github.com/apex/log v1.9.0 // indirect github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect @@ -158,4 +159,4 @@ require ( go 1.25.0 -replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9 +replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50 diff --git a/go.sum b/go.sum index 8f847efe6..1ea4600e2 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f/go.mod h1:AQiQK github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5 h1:XeHDLbi4ku2IyYoiDlE2h627nnUsaJXGydDCR33o7Ek= +github.com/Figure1/go-intervals v0.0.0-20180124190743-0109751545d5/go.mod h1:3yFSM9nNXS5nSaIwjpF+cPfSK3sqXn3HW+KyB40nLQM= github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/SevereCloud/vksdk/v2 v2.17.0 h1:Wll63JSuBTdE0L7+V/PMn9PyhLrWSWIjX76XpWbXTFw= @@ -311,8 +313,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9 h1:i3Wc7UxeqhfoipWxigPqO1JERwd1Dq76zqm9KtgR+Ds= -github.com/sh4sh/go-xmpp v0.0.0-20260306045944-c36945baa3d9/go.mod h1:veSQsIhh/ySAtFYcNwaH+qOTtbJaH3gWOLnlK1f8pRs= +github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50 h1:rhsp1fFzf4hnYUY4E83bpzDprbGyg7oXU0CHj5rwWAk= +github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50/go.mod h1:U1T7Fv9GY+8pJpOwEpNII37JLngt31TiRHzHV3m66tc= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0= From 2b2c7717179977330b6718367d50eec660e77fbb Mon Sep 17 00:00:00 2001 From: kousu Date: Sun, 8 Mar 2026 14:23:31 -0400 Subject: [PATCH 16/16] [WIP] Reactions Only XMPP<->Discord at the moment. --- bridge/config/config.go | 2 ++ bridge/discord/discord.go | 19 +++++++++++++- bridge/discord/handlers.go | 28 +++++++++++++++++++++ bridge/xmpp/xmpp.go | 51 ++++++++++++++++++++++++++++++-------- gateway/gateway.go | 3 +++ go.mod | 2 +- go.sum | 4 +-- 7 files changed, 95 insertions(+), 14 deletions(-) diff --git a/bridge/config/config.go b/bridge/config/config.go index 80770df70..f73fc00d1 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -30,6 +30,7 @@ const ( EventUserTyping = "user_typing" EventGetChannelMembers = "get_channel_members" EventNoticeIRC = "notice_irc" + EventReaction = "reaction" ) const ParentIDNotFound = "msg-parent-not-found" @@ -46,6 +47,7 @@ type Message struct { Gateway string `json:"gateway"` ParentID string `json:"parent_id"` ParentText string `json:"parent_text"` + Reactions []string `json:"reactions"` Timestamp time.Time `json:"timestamp"` ID string `json:"id"` Extra map[string][]interface{} diff --git a/bridge/discord/discord.go b/bridge/discord/discord.go index 5b6b7e413..94c26ff78 100644 --- a/bridge/discord/discord.go +++ b/bridge/discord/discord.go @@ -226,6 +226,7 @@ func (b *Bdiscord) Connect() error { b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageTyping) b.c.AddHandler(b.messageUpdate) + b.c.AddHandler(b.messageReaction) b.c.AddHandler(b.messageDelete) b.c.AddHandler(b.messageDeleteBulk) b.c.AddHandler(b.memberAdd) @@ -278,7 +279,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { // Use webhook to send the message useWebhooks := b.shouldMessageUseWebhooks(&msg) - if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" { + if useWebhooks && msg.Event != config.EventReaction && msg.Event != config.EventMsgDelete && msg.ParentID == "" { return b.handleEventWebhook(&msg, channelID) } @@ -289,6 +290,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) { b.Log.Debugf("Broadcasting using token (API)") + if msg.Event == config.EventReaction { + // this is a reaction, not a message + b.Log.Debugf("Sending reactions: %#v", msg.Reactions) + for _, reaction := range msg.Reactions { + // TODO: should we verify that reaction is, in fact, a single emoji? + // Also: Discord has server-private emojis named like "hello:1234567654321"; can these be bridged in some generic way? + b.Log.Debugf("MessageReactionAdd(%#v, %#v, %#v)", channelID, msg.ParentID, reaction) + err := b.c.MessageReactionAdd(channelID, msg.ParentID, reaction) + if err != nil { + return "", err + } + } + // reactions don't get an ID string + return "", nil + } + // Delete message if msg.Event == config.EventMsgDelete { if msg.ID == "" { diff --git a/bridge/discord/handlers.go b/bridge/discord/handlers.go index 16bb9977f..9f77255b6 100644 --- a/bridge/discord/handlers.go +++ b/bridge/discord/handlers.go @@ -8,6 +8,34 @@ import ( "github.com/matterbridge-org/matterbridge/bridge/config" ) +func (b *Bdiscord) messageReaction(s *discordgo.Session, m *discordgo.MessageReactionAdd) { //nolint:unparam + b.Log.Debugf("Got a Discord Reaction:i %#v", m) + b.Log.Debugf("emoji = %#v", m.Emoji) + + if m.GuildID != b.guildID { + b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") + return + } + + var reaction string + if m.Emoji.ID != "" { + // Discord has server-private emojis named with an ID + // Bridged these with a generic, neutral emoji. + // XXX chosing the neutralest of emojis might be .. hard. + reaction = "⬛" + } else { + reaction = m.Emoji.Name + } + + // TODO: we need to bridge m.Emoji.User.Username or m.UserID + rmsg := config.Message{Account: b.Account, Event: config.EventReaction, ParentID: m.MessageID, Reactions: []string{reaction}} + rmsg.Channel = b.getChannelName(m.ChannelID) + + b.Log.Debugf("<= Sending reaction from %s to gateway", b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg +} + func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam if m.GuildID != b.guildID { b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 950950b9d..14da37547 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -34,8 +34,8 @@ type Bxmpp struct { xc *xmpp.Client xmppMap map[string]string connected bool - stanzaIDs *lru.Cache[string, string] - replyHeaders *lru.Cache[string, xmpp.Reply] + stanzaIDs *lru.Cache[string, string] // stanzaID -> ID + replyHeaders *lru.Cache[string, xmpp.Reply] // ID -> Reply{stanzaID, to} sync.RWMutex avatarAvailability map[string]bool @@ -158,9 +158,27 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // XEP-0461: populate reply fields if this message is a reply. var reply *xmpp.Reply + var reactions *xmpp.Reactions if msg.ParentValid() { - if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { - reply = &_reply + // either a Reaction or a Reply + // + if msg.Event == config.EventReaction { + b.Log.Debugf("relaying a Reaction; the reactions are %#v", msg.Reactions) + if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { + reactions = &xmpp.Reactions{ + ID: _reply.ID, + Reactions: msg.Reactions, + } + } + + // XXX TODO: XEP-0444 says an update requires a *full* update for all reactions from a given user. + // since the bridge is the source of ALL reactions for all users on the other side, + // it needs to track what has been sent and *resend all of them* + // also it's going to lose past reactions messages... + } else { + if _reply, ok := b.replyHeaders.Get(msg.ParentID); ok { + reply = &_reply + } } } @@ -169,11 +187,12 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { // Generate a dummy ID because to avoid collision with other internal messages msgID := xid.New().String() if _, err := b.xc.Send(xmpp.Chat{ - Type: "groupchat", - Remote: msg.Channel + "@" + b.GetString("Muc"), - Text: msg.Username + msg.Text, - OriginID: msgID, - Reply: reply, + Type: "groupchat", + Remote: msg.Channel + "@" + b.GetString("Muc"), + Text: msg.Username + msg.Text, + OriginID: msgID, + Reply: reply, + Reactions: reactions, }); err != nil { return "", err } @@ -362,6 +381,17 @@ func (b *Bxmpp) handleXMPP() error { parentText = v.Reply.Quote } + // If there was a // .... + var reactions []string + if v.Reactions != nil { + if _parentID, ok := b.stanzaIDs.Get(v.Reactions.ID); ok { + parentID = _parentID + b.Log.Debugf("Got reactions: %#v", v.Reactions) + reactions = append(reactions, v.Reactions.Reactions...) // XXX is the append() necessary? + event = config.EventReaction + } + } + rmsg := config.Message{ Username: b.parseNick(v.Remote), Text: v.Text, @@ -373,6 +403,7 @@ func (b *Bxmpp) handleXMPP() error { Event: event, ParentID: parentID, ParentText: parentText, + Reactions: reactions, } // Check if we have an action event. @@ -499,7 +530,7 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool { } // skip empty messages - if message.Text == "" { + if message.Text == "" && message.Reactions == nil { return true } diff --git a/gateway/gateway.go b/gateway/gateway.go index 7ca84e4f4..4384a90f0 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -291,6 +291,9 @@ func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool { if msg.Text != "" { return false } + if msg.Event == config.EventReaction { + return false + } if msg.Event == config.EventUserTyping { return false } diff --git a/go.mod b/go.mod index 1d457d436..757ce0fd0 100644 --- a/go.mod +++ b/go.mod @@ -159,4 +159,4 @@ require ( go 1.25.0 -replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50 +replace github.com/xmppo/go-xmpp => github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a diff --git a/go.sum b/go.sum index 1ea4600e2..85089d57f 100644 --- a/go.sum +++ b/go.sum @@ -313,8 +313,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50 h1:rhsp1fFzf4hnYUY4E83bpzDprbGyg7oXU0CHj5rwWAk= -github.com/sh4sh/go-xmpp v0.0.0-20260306054259-9e62a00d8f50/go.mod h1:U1T7Fv9GY+8pJpOwEpNII37JLngt31TiRHzHV3m66tc= +github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a h1:/Id+09ZFYZGdBqpVqMUTrRznLUpD66dOunIWeRiqMTQ= +github.com/sh4sh/go-xmpp v0.0.0-20260308194659-485ad27aa55a/go.mod h1:U1T7Fv9GY+8pJpOwEpNII37JLngt31TiRHzHV3m66tc= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0=