summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/bridge/discord
diff options
context:
space:
mode:
authorMistivia <i@mistivia.com>2025-11-02 15:27:18 +0800
committerMistivia <i@mistivia.com>2025-11-02 15:27:18 +0800
commite9c24f4af7ed56760f6db7941827d09f6db9020b (patch)
tree62128c43b883ce5e3148113350978755779bb5de /teleirc/matterbridge/bridge/discord
parent58d5e7cfda4781d8a57ec52aefd02983835c301a (diff)
add matterbridge
Diffstat (limited to 'teleirc/matterbridge/bridge/discord')
-rw-r--r--teleirc/matterbridge/bridge/discord/discord.go384
-rw-r--r--teleirc/matterbridge/bridge/discord/handlers.go281
-rw-r--r--teleirc/matterbridge/bridge/discord/handlers_test.go58
-rw-r--r--teleirc/matterbridge/bridge/discord/helpers.go269
-rw-r--r--teleirc/matterbridge/bridge/discord/helpers_test.go46
-rw-r--r--teleirc/matterbridge/bridge/discord/transmitter/transmitter.go257
-rw-r--r--teleirc/matterbridge/bridge/discord/transmitter/utils.go32
-rw-r--r--teleirc/matterbridge/bridge/discord/webhook.go148
8 files changed, 1475 insertions, 0 deletions
diff --git a/teleirc/matterbridge/bridge/discord/discord.go b/teleirc/matterbridge/bridge/discord/discord.go
new file mode 100644
index 0000000..5ae6c57
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/discord.go
@@ -0,0 +1,384 @@
+package bdiscord
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/42wim/matterbridge/bridge"
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/discord/transmitter"
+ "github.com/42wim/matterbridge/bridge/helper"
+ "github.com/bwmarrin/discordgo"
+ lru "github.com/hashicorp/golang-lru"
+)
+
+const (
+ MessageLength = 1950
+ cFileUpload = "file_upload"
+)
+
+type Bdiscord struct {
+ *bridge.Config
+
+ c *discordgo.Session
+
+ nick string
+ userID string
+ guildID string
+
+ channelsMutex sync.RWMutex
+ channels []*discordgo.Channel
+ channelInfoMap map[string]*config.ChannelInfo
+
+ membersMutex sync.RWMutex
+ userMemberMap map[string]*discordgo.Member
+ nickMemberMap map[string]*discordgo.Member
+
+ // Webhook specific logic
+ useAutoWebhooks bool
+ transmitter *transmitter.Transmitter
+ cache *lru.Cache
+}
+
+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)
+ }
+
+ b := &Bdiscord{
+ Config: cfg,
+ cache: newCache,
+ }
+
+ b.userMemberMap = make(map[string]*discordgo.Member)
+ b.nickMemberMap = make(map[string]*discordgo.Member)
+ b.channelInfoMap = make(map[string]*config.ChannelInfo)
+
+ b.useAutoWebhooks = b.GetBool("AutoWebhooks")
+ if b.useAutoWebhooks {
+ b.Log.Debug("Using automatic webhooks")
+ }
+ return b
+}
+
+func (b *Bdiscord) Connect() error {
+ var err error
+ token := b.GetString("Token")
+ b.Log.Info("Connecting")
+ if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
+ token = "Bot " + b.GetString("Token")
+ }
+ // if we have a User token, remove the `Bot` prefix
+ if strings.HasPrefix(b.GetString("Token"), "User ") {
+ token = strings.Replace(b.GetString("Token"), "User ", "", -1)
+ }
+
+ b.c, err = discordgo.New(token)
+ if err != nil {
+ return err
+ }
+ b.Log.Info("Connection succeeded")
+ b.c.AddHandler(b.messageCreate)
+ b.c.AddHandler(b.messageTyping)
+ b.c.AddHandler(b.messageUpdate)
+ b.c.AddHandler(b.messageDelete)
+ b.c.AddHandler(b.messageDeleteBulk)
+ b.c.AddHandler(b.memberAdd)
+ b.c.AddHandler(b.memberRemove)
+ b.c.AddHandler(b.memberUpdate)
+ if b.GetInt("debuglevel") == 1 {
+ b.c.AddHandler(b.messageEvent)
+ }
+ // Add privileged intent for guild member tracking. This is needed to track nicks
+ // for display names and @mention translation
+ b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
+ discordgo.IntentsGuildMembers)
+
+ err = b.c.Open()
+ if err != nil {
+ return err
+ }
+ guilds, err := b.c.UserGuilds(100, "", "")
+ if err != nil {
+ return err
+ }
+ userinfo, err := b.c.User("@me")
+ if err != nil {
+ return err
+ }
+ serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
+ b.nick = userinfo.Username
+ b.userID = userinfo.ID
+
+ // Try and find this account's guild, and populate channels
+ b.channelsMutex.Lock()
+ for _, guild := range guilds {
+ // Skip, if the server name does not match the visible name or the ID
+ if guild.Name != serverName && guild.ID != serverName {
+ continue
+ }
+
+ // Complain about an ambiguous Server setting. Two Discord servers could have the same title!
+ // For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
+ if b.guildID != "" {
+ return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
+ }
+
+ // Getting this guild's channel could result in a permission error
+ b.channels, err = b.c.GuildChannels(guild.ID)
+ if err != nil {
+ return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
+ }
+
+ b.guildID = guild.ID
+ }
+ b.channelsMutex.Unlock()
+
+ // If we couldn't find a guild, we print extra debug information and return a nice error
+ if b.guildID == "" {
+ err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
+ b.Log.Error(err.Error())
+
+ // Print all of the possible server values
+ b.Log.Info("Possible server values:")
+ for _, guild := range guilds {
+ b.Log.Infof("\t- Server=%#v # by name", guild.Name)
+ b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
+ }
+
+ // If there are no results, we should say that
+ if len(guilds) == 0 {
+ b.Log.Info("\t- (none found)")
+ }
+
+ return err
+ }
+
+ // Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
+ // but we stopped doing that due to Discord making rate limits more aggressive.
+ //
+ // Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
+ // This is no longer possible.
+ if b.GetString("WebhookURL") != "" {
+ message := "The global WebhookURL setting has been removed. "
+ message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
+ message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
+ b.Log.Errorln(message)
+ return fmt.Errorf("use of removed WebhookURL setting")
+ }
+
+ if b.GetInt("debuglevel") == 2 {
+ b.Log.Debug("enabling even more discord debug")
+ b.c.Debug = true
+ }
+
+ // Initialise webhook management
+ b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
+ b.transmitter.Log = b.Log
+
+ var webhookChannelIDs []string
+ for _, channel := range b.Channels {
+ channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
+
+ // If a WebhookURL was not explicitly provided for this channel,
+ // there are two options: just a regular bot message (ugly) or this is should be webhook sent
+ if channel.Options.WebhookURL == "" {
+ // If it should be webhook sent, we should enforce this via the transmitter
+ if b.useAutoWebhooks {
+ webhookChannelIDs = append(webhookChannelIDs, channelID)
+ }
+ continue
+ }
+
+ whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
+ if !ok {
+ return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
+ }
+
+ b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
+ ID: whID,
+ Token: whToken,
+ GuildID: b.guildID,
+ ChannelID: channelID,
+ })
+ }
+
+ if b.useAutoWebhooks {
+ err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
+ if err != nil {
+ b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
+ return err
+ }
+ }
+
+ // Obtaining guild members and initializing nickname mapping.
+ b.membersMutex.Lock()
+ defer b.membersMutex.Unlock()
+ members, err := b.c.GuildMembers(b.guildID, "", 1000)
+ if err != nil {
+ b.Log.Error("Error obtaining server members: ", err)
+ return err
+ }
+ for _, member := range members {
+ if member == nil {
+ b.Log.Warnf("Skipping missing information for a user.")
+ continue
+ }
+ b.userMemberMap[member.User.ID] = member
+ b.nickMemberMap[member.User.Username] = member
+ if member.Nick != "" {
+ b.nickMemberMap[member.Nick] = member
+ }
+ }
+ return nil
+}
+
+func (b *Bdiscord) Disconnect() error {
+ return b.c.Close()
+}
+
+func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
+ b.channelsMutex.Lock()
+ defer b.channelsMutex.Unlock()
+
+ b.channelInfoMap[channel.ID] = &channel
+ return nil
+}
+
+func (b *Bdiscord) Send(msg config.Message) (string, error) {
+ b.Log.Debugf("=> Receiving %#v", msg)
+
+ channelID := b.getChannelID(msg.Channel)
+ if channelID == "" {
+ return "", fmt.Errorf("Could not find channelID for %v", msg.Channel)
+ }
+
+ if msg.Event == config.EventUserTyping {
+ if b.GetBool("ShowUserTyping") {
+ err := b.c.ChannelTyping(channelID)
+ return "", err
+ }
+ return "", nil
+ }
+
+ // Make a action /me of the message
+ if msg.Event == config.EventUserAction {
+ msg.Text = "_" + msg.Text + "_"
+ }
+
+ // Handle prefix hint for unthreaded messages.
+ if msg.ParentNotFound() {
+ msg.ParentID = ""
+ }
+
+ // Use webhook to send the message
+ useWebhooks := b.shouldMessageUseWebhooks(&msg)
+ if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
+ return b.handleEventWebhook(&msg, channelID)
+ }
+
+ return b.handleEventBotUser(&msg, channelID)
+}
+
+// handleEventDirect handles events via the bot user
+func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
+ b.Log.Debugf("Broadcasting using token (API)")
+
+ // Delete message
+ if msg.Event == config.EventMsgDelete {
+ if msg.ID == "" {
+ return "", nil
+ }
+ err := b.c.ChannelMessageDelete(channelID, msg.ID)
+ return "", err
+ }
+
+ // Delete a file
+ if msg.Event == config.EventFileDelete {
+ if msg.ID == "" {
+ return "", nil
+ }
+
+ if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
+ err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
+ b.cache.Remove(cFileUpload + msg.ID)
+ return "", err
+ }
+
+ return "", fmt.Errorf("file %s not found", msg.ID)
+ }
+
+ // Upload a file if it exists
+ if msg.Extra != nil {
+ for _, rmsg := range helper.HandleExtra(msg, b.General) {
+ rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
+ if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
+ b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
+ }
+ }
+ // check if we have files to upload (from slack, telegram or mattermost)
+ if len(msg.Extra["file"]) > 0 {
+ return b.handleUploadFile(msg, channelID)
+ }
+ }
+
+ msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
+ msg.Text = b.replaceUserMentions(msg.Text)
+
+ // Edit message
+ if msg.ID != "" {
+ _, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
+ return msg.ID, err
+ }
+
+ m := discordgo.MessageSend{
+ Content: msg.Username + msg.Text,
+ AllowedMentions: b.getAllowedMentions(),
+ }
+
+ if msg.ParentValid() {
+ m.Reference = &discordgo.MessageReference{
+ MessageID: msg.ParentID,
+ ChannelID: channelID,
+ GuildID: b.guildID,
+ }
+ }
+
+ // Post normal message
+ res, err := b.c.ChannelMessageSendComplex(channelID, &m)
+ if err != nil {
+ return "", err
+ }
+
+ return res.ID, nil
+}
+
+// handleUploadFile handles native upload of files
+func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
+ for _, f := range msg.Extra["file"] {
+ fi := f.(config.FileInfo)
+ file := discordgo.File{
+ Name: fi.Name,
+ ContentType: "",
+ Reader: bytes.NewReader(*fi.Data),
+ }
+ m := discordgo.MessageSend{
+ Content: msg.Username + fi.Comment,
+ Files: []*discordgo.File{&file},
+ AllowedMentions: b.getAllowedMentions(),
+ }
+ res, err := b.c.ChannelMessageSendComplex(channelID, &m)
+ if err != nil {
+ return "", fmt.Errorf("file upload failed: %s", err)
+ }
+
+ // link file_upload_nativeID (file ID from the original bridge) to our upload id
+ // so that we can remove this later when it eg needs to be deleted
+ b.cache.Add(cFileUpload+fi.NativeID, res.ID)
+ }
+
+ return "", nil
+}
diff --git a/teleirc/matterbridge/bridge/discord/handlers.go b/teleirc/matterbridge/bridge/discord/handlers.go
new file mode 100644
index 0000000..34cef55
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/handlers.go
@@ -0,0 +1,281 @@
+package bdiscord
+
+import (
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/bwmarrin/discordgo"
+ "github.com/davecgh/go-spew/spew"
+)
+
+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")
+ return
+ }
+ rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
+ rmsg.Channel = b.getChannelName(m.ChannelID)
+
+ b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+ b.Remote <- rmsg
+}
+
+// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
+func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild")
+ return
+ }
+ for _, msgID := range m.Messages {
+ rmsg := config.Message{
+ Account: b.Account,
+ ID: msgID,
+ Event: config.EventMsgDelete,
+ Text: config.EventMsgDelete,
+ Channel: b.getChannelName(m.ChannelID),
+ }
+
+ b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+ b.Remote <- rmsg
+ }
+}
+
+func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
+ b.Log.Debug(spew.Sdump(m.Struct))
+}
+
+func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring messageTyping because it originates from a different guild")
+ return
+ }
+ if !b.GetBool("ShowUserTyping") {
+ return
+ }
+
+ // Ignore our own typing messages
+ if m.UserID == b.userID {
+ return
+ }
+
+ rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping}
+ rmsg.Channel = b.getChannelName(m.ChannelID)
+ b.Remote <- rmsg
+}
+
+func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild")
+ return
+ }
+ if b.GetBool("EditDisable") {
+ return
+ }
+ // only when message is actually edited
+ if m.Message.EditedTimestamp != nil {
+ b.Log.Debugf("Sending edit message")
+ m.Content += b.GetString("EditSuffix")
+ msg := &discordgo.MessageCreate{
+ Message: m.Message,
+ }
+ b.messageCreate(s, msg)
+ }
+}
+
+func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring messageCreate because it originates from a different guild")
+ return
+ }
+ var err error
+
+ // not relay our own messages
+ if m.Author.Username == b.nick {
+ return
+ }
+ // if using webhooks, do not relay if it's ours
+ if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
+ return
+ }
+
+ // add the url of the attachments to content
+ if len(m.Attachments) > 0 {
+ for _, attach := range m.Attachments {
+ m.Content = m.Content + "\n" + attach.URL
+ }
+ }
+
+ rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
+
+ b.Log.Debugf("== Receiving event %#v", m.Message)
+
+ if m.Content != "" {
+ m.Message.Content = b.replaceChannelMentions(m.Message.Content)
+ rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
+ if err != nil {
+ b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
+ rmsg.Text = m.ContentWithMentionsReplaced()
+ }
+ }
+
+ // set channel name
+ rmsg.Channel = b.getChannelName(m.ChannelID)
+
+ fromWebhook := m.WebhookID != ""
+ if !fromWebhook && !b.GetBool("UseUserName") {
+ rmsg.Username = b.getNick(m.Author, m.GuildID)
+ } else {
+ rmsg.Username = m.Author.Username
+ if !fromWebhook && b.GetBool("UseDiscriminator") {
+ rmsg.Username += "#" + m.Author.Discriminator
+ }
+ }
+
+ // if we have embedded content add it to text
+ if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
+ for _, embed := range m.Message.Embeds {
+ rmsg.Text += handleEmbed(embed)
+ }
+ }
+
+ // no empty messages
+ if rmsg.Text == "" {
+ return
+ }
+
+ // do we have a /me action
+ var ok bool
+ rmsg.Text, ok = b.replaceAction(rmsg.Text)
+ if ok {
+ rmsg.Event = config.EventUserAction
+ }
+
+ // Replace emotes
+ rmsg.Text = replaceEmotes(rmsg.Text)
+
+ // Add our parent id if it exists, and if it's not referring to a message in another channel
+ if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
+ rmsg.ParentID = ref.MessageID
+ }
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+ b.Remote <- rmsg
+}
+
+func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
+ return
+ }
+ if m.Member == nil {
+ b.Log.Warnf("Received member update with no member information: %#v", m)
+ }
+
+ b.membersMutex.Lock()
+ defer b.membersMutex.Unlock()
+
+ if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok {
+ b.Log.Debugf(
+ "%s: memberupdate: user %s (nick %s) changes nick to %s",
+ b.Account,
+ m.Member.User.Username,
+ b.userMemberMap[m.Member.User.ID].Nick,
+ m.Member.Nick,
+ )
+ delete(b.nickMemberMap, currMember.User.Username)
+ delete(b.nickMemberMap, currMember.Nick)
+ delete(b.userMemberMap, m.Member.User.ID)
+ }
+ b.userMemberMap[m.Member.User.ID] = m.Member
+ b.nickMemberMap[m.Member.User.Username] = m.Member
+ if m.Member.Nick != "" {
+ b.nickMemberMap[m.Member.Nick] = m.Member
+ }
+}
+
+func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring memberAdd because it originates from a different guild")
+ return
+ }
+ if b.GetBool("nosendjoinpart") {
+ return
+ }
+ if m.Member == nil {
+ b.Log.Warnf("Received member update with no member information: %#v", m)
+ return
+ }
+ username := m.Member.User.Username
+ if m.Member.Nick != "" {
+ username = m.Member.Nick
+ }
+
+ rmsg := config.Message{
+ Account: b.Account,
+ Event: config.EventJoinLeave,
+ Username: "system",
+ Text: username + " joins",
+ }
+ b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+ b.Remote <- rmsg
+}
+
+func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
+ if m.GuildID != b.guildID {
+ b.Log.Debugf("Ignoring memberRemove because it originates from a different guild")
+ return
+ }
+ if b.GetBool("nosendjoinpart") {
+ return
+ }
+ if m.Member == nil {
+ b.Log.Warnf("Received member update with no member information: %#v", m)
+ return
+ }
+ username := m.Member.User.Username
+ if m.Member.Nick != "" {
+ username = m.Member.Nick
+ }
+
+ rmsg := config.Message{
+ Account: b.Account,
+ Event: config.EventJoinLeave,
+ Username: "system",
+ Text: username + " leaves",
+ }
+ b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+ b.Remote <- rmsg
+}
+
+func handleEmbed(embed *discordgo.MessageEmbed) string {
+ var t []string
+ var result string
+
+ t = append(t, embed.Title)
+ t = append(t, embed.Description)
+ t = append(t, embed.URL)
+
+ i := 0
+ for _, e := range t {
+ if e == "" {
+ continue
+ }
+
+ i++
+ if i == 1 {
+ result += " embed: " + e
+ continue
+ }
+
+ result += " - " + e
+ }
+
+ if result != "" {
+ result += "\n"
+ }
+
+ return result
+}
diff --git a/teleirc/matterbridge/bridge/discord/handlers_test.go b/teleirc/matterbridge/bridge/discord/handlers_test.go
new file mode 100644
index 0000000..915d9b1
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/handlers_test.go
@@ -0,0 +1,58 @@
+package bdiscord
+
+import (
+ "testing"
+
+ "github.com/bwmarrin/discordgo"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHandleEmbed(t *testing.T) {
+ testcases := map[string]struct {
+ embed *discordgo.MessageEmbed
+ result string
+ }{
+ "allempty": {
+ embed: &discordgo.MessageEmbed{},
+ result: "",
+ },
+ "one": {
+ embed: &discordgo.MessageEmbed{
+ Title: "blah",
+ },
+ result: " embed: blah\n",
+ },
+ "two": {
+ embed: &discordgo.MessageEmbed{
+ Title: "blah",
+ Description: "blah2",
+ },
+ result: " embed: blah - blah2\n",
+ },
+ "three": {
+ embed: &discordgo.MessageEmbed{
+ Title: "blah",
+ Description: "blah2",
+ URL: "blah3",
+ },
+ result: " embed: blah - blah2 - blah3\n",
+ },
+ "twob": {
+ embed: &discordgo.MessageEmbed{
+ Description: "blah2",
+ URL: "blah3",
+ },
+ result: " embed: blah2 - blah3\n",
+ },
+ "oneb": {
+ embed: &discordgo.MessageEmbed{
+ URL: "blah3",
+ },
+ result: " embed: blah3\n",
+ },
+ }
+
+ for name, tc := range testcases {
+ assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name)
+ }
+}
diff --git a/teleirc/matterbridge/bridge/discord/helpers.go b/teleirc/matterbridge/bridge/discord/helpers.go
new file mode 100644
index 0000000..2e18f46
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/helpers.go
@@ -0,0 +1,269 @@
+package bdiscord
+
+import (
+ "errors"
+ "regexp"
+ "strings"
+ "unicode"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
+ // If AllowMention is not specified, then allow all mentions (default Discord behavior)
+ if !b.IsKeySet("AllowMention") {
+ return nil
+ }
+
+ // Otherwise, allow only the mentions that are specified
+ allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
+ for _, m := range b.GetStringSlice("AllowMention") {
+ switch m {
+ case "everyone":
+ allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
+ case "roles":
+ allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
+ case "users":
+ allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
+ }
+ }
+
+ return &discordgo.MessageAllowedMentions{
+ Parse: allowedMentionTypes,
+ }
+}
+
+func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
+ b.membersMutex.RLock()
+ defer b.membersMutex.RUnlock()
+
+ if member, ok := b.userMemberMap[user.ID]; ok {
+ if member.Nick != "" {
+ // Only return if nick is set.
+ return member.Nick
+ }
+ // Otherwise return username.
+ return user.Username
+ }
+
+ // If we didn't find nick, search for it.
+ member, err := b.c.GuildMember(guildID, user.ID)
+ if err != nil {
+ b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err)
+ return user.Username
+ } else if member == nil {
+ b.Log.Warnf("Got no information for member %#v", user)
+ return user.Username
+ }
+ b.userMemberMap[user.ID] = member
+ b.nickMemberMap[member.User.Username] = member
+ if member.Nick != "" {
+ b.nickMemberMap[member.Nick] = member
+ return member.Nick
+ }
+ return user.Username
+}
+
+func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
+ b.membersMutex.RLock()
+ defer b.membersMutex.RUnlock()
+
+ if member, ok := b.nickMemberMap[nick]; ok {
+ return member, nil
+ }
+ return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
+}
+
+func (b *Bdiscord) getChannelID(name string) string {
+ if strings.Contains(name, "/") {
+ return b.getCategoryChannelID(name)
+ }
+ b.channelsMutex.RLock()
+ defer b.channelsMutex.RUnlock()
+
+ idcheck := strings.Split(name, "ID:")
+ if len(idcheck) > 1 {
+ return idcheck[1]
+ }
+ for _, channel := range b.channels {
+ if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
+ return channel.ID
+ }
+ }
+ return ""
+}
+
+func (b *Bdiscord) getCategoryChannelID(name string) string {
+ b.channelsMutex.RLock()
+ defer b.channelsMutex.RUnlock()
+ res := strings.Split(name, "/")
+ // shouldn't happen because function should be only called from getChannelID
+ if len(res) != 2 {
+ return ""
+ }
+ catName, chanName := res[0], res[1]
+ for _, channel := range b.channels {
+ // if we have a parentID, lookup the name of that parent (category)
+ // and if it matches return it
+ if channel.Name == chanName && channel.ParentID != "" {
+ for _, cat := range b.channels {
+ if cat.ID == channel.ParentID && cat.Name == catName {
+ return channel.ID
+ }
+ }
+ }
+ }
+ return ""
+}
+
+func (b *Bdiscord) getChannelName(id string) string {
+ b.channelsMutex.RLock()
+ defer b.channelsMutex.RUnlock()
+
+ for _, c := range b.channelInfoMap {
+ if c.Name == "ID:"+id {
+ // if we have ID: specified in our gateway configuration return this
+ return c.Name
+ }
+ }
+
+ for _, channel := range b.channels {
+ if channel.ID == id {
+ return b.getCategoryChannelName(channel.Name, channel.ParentID)
+ }
+ }
+ return ""
+}
+
+func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
+ var usesCat bool
+ // do we have a category configuration in the channel config
+ for _, c := range b.channelInfoMap {
+ if strings.Contains(c.Name, "/") {
+ usesCat = true
+ break
+ }
+ }
+ // configuration without category, return the normal channel name
+ if !usesCat {
+ return name
+ }
+ // create a category/channel response
+ for _, c := range b.channels {
+ if c.ID == parentID {
+ name = c.Name + "/" + name
+ }
+ }
+ return name
+}
+
+var (
+ // See https://discordapp.com/developers/docs/reference#message-formatting.
+ channelMentionRE = regexp.MustCompile("<#[0-9]+>")
+ userMentionRE = regexp.MustCompile("@[^@\n]{1,32}")
+ emoteRE = regexp.MustCompile(`<a?(:\w+:)\d+>`)
+)
+
+func (b *Bdiscord) replaceChannelMentions(text string) string {
+ replaceChannelMentionFunc := func(match string) string {
+ channelID := match[2 : len(match)-1]
+ channelName := b.getChannelName(channelID)
+
+ // If we don't have the channel refresh our list.
+ if channelName == "" {
+ var err error
+ b.channels, err = b.c.GuildChannels(b.guildID)
+ if err != nil {
+ return "#unknownchannel"
+ }
+ channelName = b.getChannelName(channelID)
+ }
+ return "#" + channelName
+ }
+ return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
+}
+
+func (b *Bdiscord) replaceUserMentions(text string) string {
+ replaceUserMentionFunc := func(match string) string {
+ var (
+ err error
+ member *discordgo.Member
+ username string
+ )
+
+ usernames := enumerateUsernames(match[1:])
+ for _, username = range usernames {
+ b.Log.Debugf("Testing mention: '%s'", username)
+ member, err = b.getGuildMemberByNick(username)
+ if err == nil {
+ break
+ }
+ }
+ if member == nil {
+ return match
+ }
+ return strings.Replace(match, "@"+username, member.User.Mention(), 1)
+ }
+ return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
+}
+
+func replaceEmotes(text string) string {
+ return emoteRE.ReplaceAllString(text, "$1")
+}
+
+func (b *Bdiscord) replaceAction(text string) (string, bool) {
+ length := len(text)
+ if length > 1 && text[0] == '_' && text[length-1] == '_' {
+ return text[1 : length-1], true
+ }
+ return text, false
+}
+
+// splitURL splits a webhookURL and returns the ID and token.
+func (b *Bdiscord) splitURL(url string) (string, string, bool) {
+ const (
+ expectedWebhookSplitCount = 7
+ webhookIdxID = 5
+ webhookIdxToken = 6
+ )
+ webhookURLSplit := strings.Split(url, "/")
+ if len(webhookURLSplit) != expectedWebhookSplitCount {
+ return "", "", false
+ }
+ return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
+}
+
+func enumerateUsernames(s string) []string {
+ onlySpace := true
+ for _, r := range s {
+ if !unicode.IsSpace(r) {
+ onlySpace = false
+ break
+ }
+ }
+ if onlySpace {
+ return nil
+ }
+
+ var username, endSpace string
+ var usernames []string
+ skippingSpace := true
+ for _, r := range s {
+ if unicode.IsSpace(r) {
+ if !skippingSpace {
+ usernames = append(usernames, username)
+ skippingSpace = true
+ }
+ endSpace += string(r)
+ username += string(r)
+ } else {
+ endSpace = ""
+ username += string(r)
+ skippingSpace = false
+ }
+ }
+ if endSpace == "" {
+ usernames = append(usernames, username)
+ }
+ return usernames
+}
diff --git a/teleirc/matterbridge/bridge/discord/helpers_test.go b/teleirc/matterbridge/bridge/discord/helpers_test.go
new file mode 100644
index 0000000..0f196d9
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/helpers_test.go
@@ -0,0 +1,46 @@
+package bdiscord
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEnumerateUsernames(t *testing.T) {
+ testcases := map[string]struct {
+ match string
+ expectedUsernames []string
+ }{
+ "only space": {
+ match: " \t\n \t",
+ expectedUsernames: nil,
+ },
+ "single word": {
+ match: "veni",
+ expectedUsernames: []string{"veni"},
+ },
+ "single word with preceeding space": {
+ match: " vidi",
+ expectedUsernames: []string{" vidi"},
+ },
+ "single word with suffixed space": {
+ match: "vici ",
+ expectedUsernames: []string{"vici"},
+ },
+ "multi-word with varying whitespace": {
+ match: "just me and\tmy friends \t",
+ expectedUsernames: []string{
+ "just",
+ "just me",
+ "just me and",
+ "just me and\tmy",
+ "just me and\tmy friends",
+ },
+ },
+ }
+
+ for testname, testcase := range testcases {
+ foundUsernames := enumerateUsernames(testcase.match)
+ assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname)
+ }
+}
diff --git a/teleirc/matterbridge/bridge/discord/transmitter/transmitter.go b/teleirc/matterbridge/bridge/discord/transmitter/transmitter.go
new file mode 100644
index 0000000..71407a1
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/transmitter/transmitter.go
@@ -0,0 +1,257 @@
+// Package transmitter provides functionality for transmitting
+// arbitrary webhook messages to Discord.
+//
+// The package provides the following functionality:
+//
+// - Creating new webhooks, whenever necessary
+// - Loading webhooks that we have previously created
+// - Sending new messages
+// - Editing messages, via message ID
+// - Deleting messages, via message ID
+//
+// The package has been designed for matterbridge, but with other
+// Go bots in mind. The public API should be matterbridge-agnostic.
+package transmitter
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bwmarrin/discordgo"
+ log "github.com/sirupsen/logrus"
+)
+
+// A Transmitter represents a message manager for a single guild.
+type Transmitter struct {
+ session *discordgo.Session
+ guild string
+ title string
+ autoCreate bool
+
+ // channelWebhooks maps from a channel ID to a webhook instance
+ channelWebhooks map[string]*discordgo.Webhook
+
+ mutex sync.RWMutex
+
+ Log *log.Entry
+}
+
+// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
+var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
+
+// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
+//
+// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
+// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
+var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
+
+// New returns a new Transmitter given a Discord session, guild ID, and title.
+func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
+ return &Transmitter{
+ session: session,
+ guild: guild,
+ title: title,
+ autoCreate: autoCreate,
+
+ channelWebhooks: make(map[string]*discordgo.Webhook),
+
+ Log: log.NewEntry(log.StandardLogger()),
+ }
+}
+
+// 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) {
+ wh, err := t.getOrCreateWebhook(channelID)
+ if err != nil {
+ return nil, err
+ }
+
+ msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
+ if err != nil {
+ return nil, fmt.Errorf("execute failed: %w", err)
+ }
+
+ return msg, nil
+}
+
+// Edit will edit a message in a channel, if possible.
+func (t *Transmitter) Edit(channelID 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
+ _, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// HasWebhook checks whether the transmitter is using a particular webhook.
+func (t *Transmitter) HasWebhook(id string) bool {
+ t.mutex.RLock()
+ defer t.mutex.RUnlock()
+
+ for _, wh := range t.channelWebhooks {
+ if wh.ID == id {
+ return true
+ }
+ }
+
+ return false
+}
+
+// AddWebhook allows you to register a channel's webhook with the transmitter.
+func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
+ t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
+ t.mutex.Lock()
+ defer t.mutex.Unlock()
+
+ _, replaced := t.channelWebhooks[channelID]
+ t.channelWebhooks[channelID] = webhook
+ return replaced
+}
+
+// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
+//
+// Notes:
+//
+// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
+// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
+// - This function is additive and will not unload previously loaded webhooks.
+// - A nil channelIDs slice is treated the same as an empty one.
+//
+// If the bot has guild-wide permission:
+//
+// 1. it will load any "relevant" webhooks from the entire guild
+// 2. the given slice is ignored
+//
+// If the bot does not have guild-wide permission:
+//
+// 1. it will load any "relevant" webhooks in each channel
+// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
+//
+// If any channel has more than one "relevant" webhook, it will randomly pick one.
+func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
+ t.Log.Debugln("Refreshing guild webhooks")
+
+ botID, err := getDiscordUserID(t.session)
+ if err != nil {
+ return fmt.Errorf("could not get current user: %w", err)
+ }
+
+ // Get all existing webhooks
+ hooks, err := t.session.GuildWebhooks(t.guild)
+ if err != nil {
+ switch {
+ case isDiscordPermissionError(err):
+ // We fallback on manually fetching hooks from individual channels
+ // if we don't have the "Manage Webhooks" permission globally.
+ // We can only do this if we were provided channelIDs, though.
+ if len(channelIDs) == 0 {
+ return ErrPermissionDenied
+ }
+ t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
+ return t.fetchChannelsHooks(channelIDs, botID)
+ default:
+ return fmt.Errorf("could not get webhooks: %w", err)
+ }
+ }
+
+ t.Log.Debugln("Refreshing guild webhooks using global permission")
+ t.assignHooksByAppID(hooks, botID, false)
+ return nil
+}
+
+// createWebhook creates a webhook for a specific channel.
+func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
+ t.mutex.Lock()
+ defer t.mutex.Unlock()
+
+ wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
+ if err != nil {
+ return nil, err
+ }
+
+ t.channelWebhooks[channel] = wh
+ return wh, nil
+}
+
+func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
+ t.mutex.RLock()
+ defer t.mutex.RUnlock()
+
+ return t.channelWebhooks[channel]
+}
+
+func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
+ // If we have a webhook for this channel, immediately return it
+ wh := t.getWebhook(channelID)
+ if wh != nil {
+ return wh, nil
+ }
+
+ // Early exit if we don't want to automatically create one
+ if !t.autoCreate {
+ return nil, ErrWebhookNotFound
+ }
+
+ t.Log.Infof("Creating a webhook for %s\n", channelID)
+ wh, err := t.createWebhook(channelID)
+ if err != nil {
+ return nil, fmt.Errorf("could not create webhook: %w", err)
+ }
+
+ return wh, nil
+}
+
+// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
+func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
+ // For each channel, search for relevant hooks
+ var failedHooks []string
+ for _, channelID := range channelIDs {
+ hooks, err := t.session.ChannelWebhooks(channelID)
+ if err != nil {
+ failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
+ continue
+ }
+ t.assignHooksByAppID(hooks, botID, true)
+ }
+
+ // Compose an error if any hooks failed
+ if len(failedHooks) > 0 {
+ return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
+ }
+
+ return nil
+}
+
+func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
+ logLine := "Picking up webhook"
+ if channelTargeted {
+ logLine += " (channel targeted)"
+ }
+
+ t.mutex.Lock()
+ defer t.mutex.Unlock()
+
+ for _, wh := range hooks {
+ if wh.ApplicationID != appID {
+ continue
+ }
+
+ t.channelWebhooks[wh.ChannelID] = wh
+ t.Log.WithFields(log.Fields{
+ "id": wh.ID,
+ "name": wh.Name,
+ "channel": wh.ChannelID,
+ }).Println(logLine)
+ }
+}
diff --git a/teleirc/matterbridge/bridge/discord/transmitter/utils.go b/teleirc/matterbridge/bridge/discord/transmitter/utils.go
new file mode 100644
index 0000000..042aa50
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/transmitter/utils.go
@@ -0,0 +1,32 @@
+package transmitter
+
+import (
+ "github.com/bwmarrin/discordgo"
+)
+
+// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
+func isDiscordPermissionError(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ restErr, ok := err.(*discordgo.RESTError)
+ if !ok {
+ return false
+ }
+
+ return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
+}
+
+// getDiscordUserID gets own user ID from state, and fallback on API request
+func getDiscordUserID(session *discordgo.Session) (string, error) {
+ if user := session.State.User; user != nil {
+ return user.ID, nil
+ }
+
+ user, err := session.User("@me")
+ if err != nil {
+ return "", err
+ }
+ return user.ID, nil
+}
diff --git a/teleirc/matterbridge/bridge/discord/webhook.go b/teleirc/matterbridge/bridge/discord/webhook.go
new file mode 100644
index 0000000..c34fc94
--- /dev/null
+++ b/teleirc/matterbridge/bridge/discord/webhook.go
@@ -0,0 +1,148 @@
+package bdiscord
+
+import (
+ "bytes"
+
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/helper"
+ "github.com/bwmarrin/discordgo"
+)
+
+// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
+func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
+ if b.useAutoWebhooks {
+ return true
+ }
+
+ b.channelsMutex.RLock()
+ defer b.channelsMutex.RUnlock()
+ if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
+ if ci.Options.WebhookURL != "" {
+ return true
+ }
+ }
+ return false
+}
+
+// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
+// account or protocol, and if so, returns the Discord avatar (if exists)
+func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
+ for _, val := range b.GetStringSlice("UseLocalAvatar") {
+ if msg.Protocol != val && msg.Account != val {
+ continue
+ }
+
+ member, err := b.getGuildMemberByNick(msg.Username)
+ if err != nil {
+ return ""
+ }
+
+ return member.User.AvatarURL("")
+ }
+ return ""
+}
+
+// webhookSend send one or more message via webhook, taking care of file
+// uploads (from slack, telegram or mattermost).
+// Returns messageID and error.
+func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
+ var (
+ res *discordgo.Message
+ err error
+ )
+
+ // If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
+ if msg.Avatar == "" {
+ msg.Avatar = b.maybeGetLocalAvatar(msg)
+ }
+
+ // WebhookParams can have either `Content` or `File`.
+
+ // We can't send empty messages.
+ if msg.Text != "" {
+ res, err = b.transmitter.Send(
+ channelID,
+ &discordgo.WebhookParams{
+ Content: msg.Text,
+ Username: msg.Username,
+ AvatarURL: msg.Avatar,
+ AllowedMentions: b.getAllowedMentions(),
+ },
+ )
+ if err != nil {
+ b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
+ }
+ }
+
+ if msg.Extra != nil {
+ for _, f := range msg.Extra["file"] {
+ fi := f.(config.FileInfo)
+ file := discordgo.File{
+ Name: fi.Name,
+ ContentType: "",
+ Reader: bytes.NewReader(*fi.Data),
+ }
+ content := fi.Comment
+
+ _, e2 := b.transmitter.Send(
+ channelID,
+ &discordgo.WebhookParams{
+ Username: msg.Username,
+ AvatarURL: msg.Avatar,
+ Files: []*discordgo.File{&file},
+ Content: content,
+ AllowedMentions: b.getAllowedMentions(),
+ },
+ )
+ if e2 != nil {
+ b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
+ }
+ }
+ }
+ return res, err
+}
+
+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 {
+ return "", nil
+ }
+
+ // skip empty messages
+ if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
+ b.Log.Debugf("Skipping empty message %#v", msg)
+ return "", nil
+ }
+
+ msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
+ msg.Text = b.replaceUserMentions(msg.Text)
+ // discord username must be [0..32] max
+ if len(msg.Username) > 32 {
+ msg.Username = msg.Username[0:32]
+ }
+
+ if msg.ID != "" {
+ b.Log.Debugf("Editing webhook message")
+ err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
+ Content: msg.Text,
+ Username: msg.Username,
+ AllowedMentions: b.getAllowedMentions(),
+ })
+ if err == nil {
+ return msg.ID, nil
+ }
+ b.Log.Errorf("Could not edit webhook message: %s", err)
+ }
+
+ b.Log.Debugf("Processing webhook sending for message %#v", msg)
+ discordMsg, err := b.webhookSend(msg, channelID)
+ if err != nil {
+ b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
+ return "", err
+ }
+ if discordMsg == nil {
+ return "", nil
+ }
+
+ return discordMsg.ID, nil
+}