From beb2c614dd0f204e8dffc7d80ee7438a981d6069 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 17 Feb 2026 19:01:27 -0500 Subject: [PATCH] discord: Workaround replies not being allowed to use webhooks. Discord does not allow replies to be created via webhook - https://github.com/discord/discord-api-docs/issues/2251 - https://github.com/discord/discord-api-docs/discussions/3282 this means that, in a standard setup where Autowebhooks=true is on, a conversation that interleaves replies cannot have the bridge masquerade the username on the replies and if RemoteNickFormat={NICK}, this leads to messages running together with sender like [reply to username1] bot: username2msgbody This work-around treats spoofed usernames as if they were always RemoteNickFormat="{NICK}", while in the non-spoofed (i.e. reply or otherwise) cases, it respects the operator's `RemoteNickFormat` so they can set e.g. RemoteNickFormat="{NICK}: ". --- bridge/config/config.go | 27 ++++++++++++++------------- bridge/discord/webhook.go | 12 ++++++------ changelog.md | 2 ++ docs/protocols/discord/README.md | 2 ++ gateway/gateway.go | 1 + 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/bridge/config/config.go b/bridge/config/config.go index 994b83488..76deee6b0 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"` + OriginalUsername string `json:"original_username"` // Username before RemoteNickFormat gets applied + 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{} } func (m Message) ParentNotFound() bool { diff --git a/bridge/discord/webhook.go b/bridge/discord/webhook.go index 8dadf5f23..1a68cfb13 100644 --- a/bridge/discord/webhook.go +++ b/bridge/discord/webhook.go @@ -33,7 +33,7 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string { continue } - member, err := b.getGuildMemberByNick(msg.Username) + member, err := b.getGuildMemberByNick(msg.OriginalUsername) if err != nil { return "" } @@ -51,7 +51,7 @@ func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (s channelID, &discordgo.WebhookParams{ Content: msgPart, - Username: msg.Username, + Username: msg.OriginalUsername, AvatarURL: msg.Avatar, AllowedMentions: b.getAllowedMentions(), }, @@ -81,7 +81,7 @@ func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) e _, err := b.transmitter.Send( channelID, &discordgo.WebhookParams{ - Username: msg.Username, + Username: msg.OriginalUsername, AvatarURL: msg.Avatar, Files: []*discordgo.File{&file}, Content: content, @@ -137,8 +137,8 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st } // discord username must be [0..32] max - if len(msg.Username) > 32 { - msg.Username = msg.Username[0:32] + if len(msg.OriginalUsername) > 32 { + msg.OriginalUsername = msg.OriginalUsername[0:32] } if msg.ID != "" { @@ -155,7 +155,7 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st // TODO: Optimize away noop-updates of un-edited messages editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{ Content: msgParts[i], - Username: msg.Username, + Username: msg.OriginalUsername, AllowedMentions: b.getAllowedMentions(), }) if editErr != nil { diff --git a/changelog.md b/changelog.md index a61154562..b3e169401 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ ## New Features - general + - The original username of a message is carried internally beside the version modified by `RemoteNickFormat` ([#135](https://github.com/matterbridge-org/matterbridge/pull/135)) for bridges that can make use of it. - matterbridge output now colors log level for easier log reading ([#25](https://github.com/matterbridge-org/matterbridge/pull/25)) - new HTTP helpers are common to all bridges, and allow overriding specific settings ([#59](https://github.com/matterbridge-org/matterbridge/pull/59)) - matterbridge is now built with whatsappmulti backend enabled by default, unless the `nowhatsappmulti` build tag is passed @@ -41,6 +42,7 @@ - Can now upload files from bytes in addition to sharing attachement URLs ([#23](https://github.com/matterbridge-org/matterbridge/pull/23/)) - Can now receive and download OOB attachments from XMPP channels to share with other bridges ([#23](https://github.com/matterbridge-org/matterbridge/pull/23/)) - discord + - Messages that use the username-spoofing webhook API always use the original username instead of applying `RemoteNickFormat`. - Replies will be included inline ([#124](https://github.com/matterbridge-org/matterbridge/pull/124), thanks @lekoOwO), by default like "(re name: message)". This is useful when bridging to destinations that do not understand replies, but distracting when the destination does. Can be disabled with `QuoteDisable=true` under your `[discord]` config. - whatsapp - legacy `whatsapp` backend has been deprecated in favor of `whatsappmulti` ([#32](https://github.com/matterbridge-org/matterbridge/issues/32)) ; this is not a breaking change and will not affect your existing settings diff --git a/docs/protocols/discord/README.md b/docs/protocols/discord/README.md index cb4d978e0..bc9212c98 100644 --- a/docs/protocols/discord/README.md +++ b/docs/protocols/discord/README.md @@ -32,6 +32,8 @@ See [account.md](account.md). [Creating a message](https://discordapp.com/developers/docs/resources/channel#create-message) via a user's API token (the basic configuration above) only lets Matterbridge post with the user/avatar that generated the token. But [executing a webhook](https://discordapp.com/developers/docs/resources/webhook#execute-webhook) can set any username and avatar URL. +However, _replies_ [cannot spo](https://github.com/discord/discord-api-docs/issues/2251)[oof usernames](https://github.com/discord/discord-api-docs/discussions/3282), so unfortunately replies always fall back to embedding the sender's username with `RemoteNickFormat`. + If you grant the bot the "Manage Webhooks" permission, it will automatically load and create webhooks in every bridged channel. You can even grant that permission on specific channels, if you don't want to give it global permission. 1. Server Settings -> Roles diff --git a/gateway/gateway.go b/gateway/gateway.go index 7ca84e4f4..d58e04d1f 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -476,6 +476,7 @@ func (gw *Gateway) SendMessage( msg.Channel = channel.Name msg.Avatar = gw.modifyAvatar(rmsg, dest) + msg.OriginalUsername = msg.Username msg.Username = gw.modifyUsername(rmsg, dest) // exclude file delete event as the msg ID here is the native file ID that needs to be deleted