forked from jshiffer/matterbridge
2a7f28606c
Without this declared, it seems that Discord will not send any member update events after connection, even if the privileged gateway intent is enabled for the bot in settings. This causes nick tracking to get out of sync when people change their nicks after the bot connects. See: https://discord.com/developers/docs/topics/gateway#gateway-intents
348 lines
9.6 KiB
Go
348 lines
9.6 KiB
Go
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/matterbridge/discordgo"
|
|
)
|
|
|
|
const MessageLength = 1950
|
|
|
|
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
|
|
}
|
|
|
|
func New(cfg *bridge.Config) bridge.Bridger {
|
|
b := &Bdiscord{Config: cfg}
|
|
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.memberUpdate)
|
|
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)
|
|
// 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") > 0 {
|
|
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 = ""
|
|
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
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)
|
|
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,
|
|
}
|
|
|
|
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) {
|
|
var err 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},
|
|
}
|
|
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
|
|
if err != nil {
|
|
return "", fmt.Errorf("file upload failed: %s", err)
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|