1package bdiscord
2
3import (
4	"errors"
5	"regexp"
6	"strings"
7	"unicode"
8
9	"github.com/matterbridge/discordgo"
10)
11
12func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
13	// If AllowMention is not specified, then allow all mentions (default Discord behavior)
14	if !b.IsKeySet("AllowMention") {
15		return nil
16	}
17
18	// Otherwise, allow only the mentions that are specified
19	allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
20	for _, m := range b.GetStringSlice("AllowMention") {
21		switch m {
22		case "everyone":
23			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
24		case "roles":
25			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
26		case "users":
27			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
28		}
29	}
30
31	return &discordgo.MessageAllowedMentions{
32		Parse: allowedMentionTypes,
33	}
34}
35
36func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
37	b.membersMutex.RLock()
38	defer b.membersMutex.RUnlock()
39
40	if member, ok := b.userMemberMap[user.ID]; ok {
41		if member.Nick != "" {
42			// Only return if nick is set.
43			return member.Nick
44		}
45		// Otherwise return username.
46		return user.Username
47	}
48
49	// If we didn't find nick, search for it.
50	member, err := b.c.GuildMember(guildID, user.ID)
51	if err != nil {
52		b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err)
53		return user.Username
54	} else if member == nil {
55		b.Log.Warnf("Got no information for member %#v", user)
56		return user.Username
57	}
58	b.userMemberMap[user.ID] = member
59	b.nickMemberMap[member.User.Username] = member
60	if member.Nick != "" {
61		b.nickMemberMap[member.Nick] = member
62		return member.Nick
63	}
64	return user.Username
65}
66
67func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
68	b.membersMutex.RLock()
69	defer b.membersMutex.RUnlock()
70
71	if member, ok := b.nickMemberMap[nick]; ok {
72		return member, nil
73	}
74	return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
75}
76
77func (b *Bdiscord) getChannelID(name string) string {
78	if strings.Contains(name, "/") {
79		return b.getCategoryChannelID(name)
80	}
81	b.channelsMutex.RLock()
82	defer b.channelsMutex.RUnlock()
83
84	idcheck := strings.Split(name, "ID:")
85	if len(idcheck) > 1 {
86		return idcheck[1]
87	}
88	for _, channel := range b.channels {
89		if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
90			return channel.ID
91		}
92	}
93	return ""
94}
95
96func (b *Bdiscord) getCategoryChannelID(name string) string {
97	b.channelsMutex.RLock()
98	defer b.channelsMutex.RUnlock()
99	res := strings.Split(name, "/")
100	// shouldn't happen because function should be only called from getChannelID
101	if len(res) != 2 {
102		return ""
103	}
104	catName, chanName := res[0], res[1]
105	for _, channel := range b.channels {
106		// if we have a parentID, lookup the name of that parent (category)
107		// and if it matches return it
108		if channel.Name == chanName && channel.ParentID != "" {
109			for _, cat := range b.channels {
110				if cat.ID == channel.ParentID && cat.Name == catName {
111					return channel.ID
112				}
113			}
114		}
115	}
116	return ""
117}
118
119func (b *Bdiscord) getChannelName(id string) string {
120	b.channelsMutex.RLock()
121	defer b.channelsMutex.RUnlock()
122
123	for _, c := range b.channelInfoMap {
124		if c.Name == "ID:"+id {
125			// if we have ID: specified in our gateway configuration return this
126			return c.Name
127		}
128	}
129
130	for _, channel := range b.channels {
131		if channel.ID == id {
132			return b.getCategoryChannelName(channel.Name, channel.ParentID)
133		}
134	}
135	return ""
136}
137
138func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
139	var usesCat bool
140	// do we have a category configuration in the channel config
141	for _, c := range b.channelInfoMap {
142		if strings.Contains(c.Name, "/") {
143			usesCat = true
144			break
145		}
146	}
147	// configuration without category, return the normal channel name
148	if !usesCat {
149		return name
150	}
151	// create a category/channel response
152	for _, c := range b.channels {
153		if c.ID == parentID {
154			name = c.Name + "/" + name
155		}
156	}
157	return name
158}
159
160var (
161	// See https://discordapp.com/developers/docs/reference#message-formatting.
162	channelMentionRE = regexp.MustCompile("<#[0-9]+>")
163	userMentionRE    = regexp.MustCompile("@[^@\n]{1,32}")
164	emoteRE          = regexp.MustCompile(`<a?(:\w+:)\d+>`)
165)
166
167func (b *Bdiscord) replaceChannelMentions(text string) string {
168	replaceChannelMentionFunc := func(match string) string {
169		channelID := match[2 : len(match)-1]
170		channelName := b.getChannelName(channelID)
171
172		// If we don't have the channel refresh our list.
173		if channelName == "" {
174			var err error
175			b.channels, err = b.c.GuildChannels(b.guildID)
176			if err != nil {
177				return "#unknownchannel"
178			}
179			channelName = b.getChannelName(channelID)
180		}
181		return "#" + channelName
182	}
183	return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
184}
185
186func (b *Bdiscord) replaceUserMentions(text string) string {
187	replaceUserMentionFunc := func(match string) string {
188		var (
189			err      error
190			member   *discordgo.Member
191			username string
192		)
193
194		usernames := enumerateUsernames(match[1:])
195		for _, username = range usernames {
196			b.Log.Debugf("Testing mention: '%s'", username)
197			member, err = b.getGuildMemberByNick(username)
198			if err == nil {
199				break
200			}
201		}
202		if member == nil {
203			return match
204		}
205		return strings.Replace(match, "@"+username, member.User.Mention(), 1)
206	}
207	return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
208}
209
210func replaceEmotes(text string) string {
211	return emoteRE.ReplaceAllString(text, "$1")
212}
213
214func (b *Bdiscord) replaceAction(text string) (string, bool) {
215	length := len(text)
216	if length > 1 && text[0] == '_' && text[length-1] == '_' {
217		return text[1 : length-1], true
218	}
219	return text, false
220}
221
222// splitURL splits a webhookURL and returns the ID and token.
223func (b *Bdiscord) splitURL(url string) (string, string, bool) {
224	const (
225		expectedWebhookSplitCount = 7
226		webhookIdxID              = 5
227		webhookIdxToken           = 6
228	)
229	webhookURLSplit := strings.Split(url, "/")
230	if len(webhookURLSplit) != expectedWebhookSplitCount {
231		return "", "", false
232	}
233	return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
234}
235
236func enumerateUsernames(s string) []string {
237	onlySpace := true
238	for _, r := range s {
239		if !unicode.IsSpace(r) {
240			onlySpace = false
241			break
242		}
243	}
244	if onlySpace {
245		return nil
246	}
247
248	var username, endSpace string
249	var usernames []string
250	skippingSpace := true
251	for _, r := range s {
252		if unicode.IsSpace(r) {
253			if !skippingSpace {
254				usernames = append(usernames, username)
255				skippingSpace = true
256			}
257			endSpace += string(r)
258			username += string(r)
259		} else {
260			endSpace = ""
261			username += string(r)
262			skippingSpace = false
263		}
264	}
265	if endSpace == "" {
266		usernames = append(usernames, username)
267	}
268	return usernames
269}
270