Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions bridge/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,27 @@ const (
EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members"
EventNoticeIRC = "notice_irc"
EventReaction = "reaction"
)

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"`
Reactions []string `json:"reactions"`
Timestamp time.Time `json:"timestamp"`
ID string `json:"id"`
Extra map[string][]interface{}
}

func (m Message) ParentNotFound() bool {
Expand Down
19 changes: 18 additions & 1 deletion bridge/discord/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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 == "" {
Expand Down
28 changes: 28 additions & 0 deletions bridge/discord/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
118 changes: 95 additions & 23 deletions bridge/xmpp/xmpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"
"time"

lru "github.com/hashicorp/golang-lru/v2"
"github.com/jpillora/backoff"
"github.com/matterbridge-org/matterbridge/bridge"
"github.com/matterbridge-org/matterbridge/bridge/config"
Expand All @@ -29,10 +30,12 @@ type UploadBufferEntry struct {
type Bxmpp struct {
*bridge.Config

startTime time.Time
xc *xmpp.Client
xmppMap map[string]string
connected bool
startTime time.Time
xc *xmpp.Client
xmppMap map[string]string
connected bool
stanzaIDs *lru.Cache[string, string] // stanzaID -> ID
replyHeaders *lru.Cache[string, xmpp.Reply] // ID -> Reply{stanzaID, to}
sync.RWMutex

avatarAvailability map[string]bool
Expand All @@ -58,12 +61,23 @@ type Bxmpp struct {
}

func New(cfg *bridge.Config) bridge.Bridger {
stanzaIDs, err := lru.New[string, string](5000)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache: %v", err)
}
replyHeaders, err := lru.New[string, xmpp.Reply](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),
stanzaIDs: stanzaIDs,
replyHeaders: replyHeaders,
}
}

Expand Down Expand Up @@ -142,19 +156,46 @@ 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() {
// 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
}
}
}

// 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",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
OriginID: msgID,
Reply: reply,
Reactions: reactions,
}); 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
}

Expand Down Expand Up @@ -300,6 +341,13 @@ func (b *Bxmpp) handleXMPP() error {
if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v)

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})
}

// Skip invalid messages.
if b.skipMessage(v) {
continue
Expand All @@ -320,18 +368,42 @@ func (b *Bxmpp) handleXMPP() error {
avatar = getAvatar(b.avatarMap, v.Remote, b.General)
}

// If there was a <reply>, 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 != nil {
if _parentID, ok := b.stanzaIDs.Get(v.Reply.ID); ok {
parentID = _parentID
}

// 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
}

// If there was a <reactions> // ....
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,
Channel: b.parseChannel(v.Remote),
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,
Event: event,
Extra: make(map[string][]any),
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,
Reactions: reactions,
}

// Check if we have an action event.
Expand Down Expand Up @@ -458,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
}

Expand Down
3 changes: 3 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
17 changes: 10 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,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
Expand All @@ -55,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
Expand All @@ -76,7 +78,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
Expand Down Expand Up @@ -133,11 +134,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
Expand All @@ -156,4 +157,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-20260308194659-485ad27aa55a
Loading
Loading