Skip to content

Commit

Permalink
Add support for discord forum channels/threads (discord)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tomabrafix committed Sep 3, 2023
1 parent 56e7bd0 commit 3e3090f
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 12 deletions.
1 change: 1 addition & 0 deletions bridge/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ type Protocol struct {
type ChannelOptions struct {
Key string // irc, xmpp
WebhookURL string // discord
ForumID string // discord
Topic string // zulip
}

Expand Down
21 changes: 17 additions & 4 deletions bridge/discord/transmitter/transmitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,29 +63,38 @@ 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)
}

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
Expand All @@ -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()
Expand Down
29 changes: 22 additions & 7 deletions bridge/discord/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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(),
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion matterbridge.toml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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=""

Expand Down

0 comments on commit 3e3090f

Please sign in to comment.