42wim/matterbridge

View on GitHub
bridge/discord/helpers.go

Summary

Maintainability
A
0 mins
Test Coverage
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[strings.TrimSpace(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
}