From 3e3090fc53a3e095e02641b8589653631457d216 Mon Sep 17 00:00:00 2001 From: Tom Kunze Date: Sun, 6 Nov 2022 15:27:50 +0100 Subject: [PATCH] Add support for discord forum channels/threads (discord) Messages send to forum threads need to use the ID of the forum for the webhook but also need to set an additional parameter during the API call to specify the destination thread. Receiving messages from threads works by using the thread ID. The forum ID is not required. This commit adds an optional channel option "ForumID" which can be used to supply the forum ID of a thread (= channel). When sending/editing messages the ForumID will be used for the webhook and the channel ID will be supplied as the additional parameter "thread_id". Fixes #1908 --- bridge/config/config.go | 1 + bridge/discord/transmitter/transmitter.go | 21 ++++++++++++---- bridge/discord/webhook.go | 29 +++++++++++++++++------ matterbridge.toml.sample | 7 +++++- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/bridge/config/config.go b/bridge/config/config.go index 18c60920..2bb57bb4 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -181,6 +181,7 @@ type Protocol struct { type ChannelOptions struct { Key string // irc, xmpp WebhookURL string // discord + ForumID string // discord Topic string // zulip } diff --git a/bridge/discord/transmitter/transmitter.go b/bridge/discord/transmitter/transmitter.go index 71407a1d..78c8ed2f 100644 --- a/bridge/discord/transmitter/transmitter.go +++ b/bridge/discord/transmitter/transmitter.go @@ -63,13 +63,13 @@ func New(session *discordgo.Session, guild string, title string, autoCreate bool } // Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data. -func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { +func (t *Transmitter) SendThread(channelID string, threadID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { wh, err := t.getOrCreateWebhook(channelID) if err != nil { return nil, err } - msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params) + msg, err := t.session.WebhookThreadExecute(wh.ID, wh.Token, true, threadID, params) if err != nil { return nil, fmt.Errorf("execute failed: %w", err) } @@ -77,15 +77,24 @@ func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (* return msg, nil } +func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { + return t.SendThread(channelID, "", params) +} + // Edit will edit a message in a channel, if possible. -func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error { +func (t *Transmitter) EditThread(channelID string, threadID string, messageID string, params *discordgo.WebhookParams) error { wh := t.getWebhook(channelID) if wh == nil { return ErrWebhookNotFound } - uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID + threadParam := "" + if threadID != "" { + threadParam = "?thread_id=" + threadID + } + + uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID + threadParam _, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", "")) if err != nil { return err @@ -94,6 +103,10 @@ func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo return nil } +func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error { + return t.EditThread(channelID, "", messageID, params) +} + // HasWebhook checks whether the transmitter is using a particular webhook. func (t *Transmitter) HasWebhook(id string) bool { t.mutex.RLock() diff --git a/bridge/discord/webhook.go b/bridge/discord/webhook.go index b518ea62..ec009a38 100644 --- a/bridge/discord/webhook.go +++ b/bridge/discord/webhook.go @@ -42,10 +42,10 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string { return "" } -// webhookSend send one or more message via webhook, taking care of file -// uploads (from slack, telegram or mattermost). +// webhookSendThread send one or more message via webhook, taking care of file +// uploads (from slack, telegram or mattermost) and forum threads. // Returns messageID and error. -func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) { +func (b *Bdiscord) webhookSendThread(msg *config.Message, channelID string, threadID string) (*discordgo.Message, error) { var ( res *discordgo.Message res2 *discordgo.Message @@ -61,8 +61,9 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg // We can't send empty messages. if msg.Text != "" { - res, err = b.transmitter.Send( + res, err = b.transmitter.SendThread( channelID, + threadID, &discordgo.WebhookParams{ Content: msg.Text, Username: msg.Username, @@ -85,8 +86,9 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg } content := fi.Comment - res2, err = b.transmitter.Send( + res2, err = b.transmitter.SendThread( channelID, + threadID, &discordgo.WebhookParams{ Username: msg.Username, AvatarURL: msg.Avatar, @@ -108,6 +110,10 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg return res, err } +func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) { + return b.webhookSendThread(msg, channelID, "") +} + func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) { // skip events if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { @@ -120,6 +126,15 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st return "", nil } + // If ForumID is set, it should be used as channelID for the webhook and the channelID is used as the thread_id + threadID := "" + if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { + if ci.Options.ForumID != "" { + threadID = channelID + channelID = ci.Options.ForumID + } + } + msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped")) msg.Text = b.replaceUserMentions(msg.Text) // discord username must be [0..32] max @@ -129,7 +144,7 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st if msg.ID != "" { b.Log.Debugf("Editing webhook message") - err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{ + err := b.transmitter.EditThread(channelID, threadID, msg.ID, &discordgo.WebhookParams{ Content: msg.Text, Username: msg.Username, AllowedMentions: b.getAllowedMentions(), @@ -141,7 +156,7 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st } b.Log.Debugf("Processing webhook sending for message %#v", msg) - discordMsg, err := b.webhookSend(msg, channelID) + discordMsg, err := b.webhookSendThread(msg, channelID, threadID) if err != nil { b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err) return "", err diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 5932b269..b05df661 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -1928,8 +1928,13 @@ enable=true channel="mygreatgame" [gateway.inout.options] - # WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge. + # OPTIONAL - ForumID: the ID of the forum channel containing the thread. The thread ID should be used for "channel" then. + # Note: Do not add "ID:" at the beginning of the ForumID. + # Example: "123456789" + ForumID="" + # OPTIONAL - WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge. # If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config. + # WebhookURL does not support forum channels (URLs containing "?thread_id="), see "ForumID" # Example: "https://discord.com/api/webhooks/1234/abcd_xyzw" WebhookURL=""