From f83f6c49f380010a12a0f465a4df38ed3efd8d9d 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/14] 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 22feac7e32e2d8e045e80b492e558a7dcf932cea 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/14] 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 bd2868cbd90409fe79711edc348308f150d05ba0 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/14] 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 980bcfdbc85c825d54f29662c0a777d939972031 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 15:28:17 -0500 Subject: [PATCH 04/14] 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 591e02164d36166fdd90b1c2857036e3cdb7f851 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:10:29 -0500 Subject: [PATCH 05/14] 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 a7a48c4e5462799066d91cb6aaf906cff29351a8 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:12:19 -0500 Subject: [PATCH 06/14] 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 cca872ed3f03d90e6ef2e677f420787499d60744 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:18:24 -0500 Subject: [PATCH 07/14] 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 5deb9b2a000d4012e14e4bad19d15d9b273c794f Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 16:30:05 -0500 Subject: [PATCH 08/14] 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 eb86f9ccdca862acfe03113b423efe1b3bdb7887 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 17:07:34 -0500 Subject: [PATCH 09/14] 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 0627ef506cdfc1c42ef1bd9039670152b9dc2b5f 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/14] 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 3d40b3c8f384c8f22cb662472e0b8ce4f2dbbf01 Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:30:17 -0500 Subject: [PATCH 11/14] 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 74b0a932022f313a5e3c5bcfad6d0428a45cbb08 Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:21:36 -0500 Subject: [PATCH 12/14] 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 6129e66c685e7a1c4b2e113a7e50e3390679fa0d Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 23:34:47 -0500 Subject: [PATCH 13/14] 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 45d4f5bb8fd6a7a58fe4166863c322fec538f5a5 Mon Sep 17 00:00:00 2001 From: kousu Date: Fri, 6 Mar 2026 01:04:46 -0500 Subject: [PATCH 14/14] 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=