cchat-discord/internal/discord/channel/message/send/complete/completer.go

243 lines
5.7 KiB
Go

package complete
import (
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
"github.com/diamondburned/cchat-discord/internal/discord/message"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text"
)
type Completer struct {
*shared.Channel
}
const MaxCompletion = 15
func New(ch *shared.Channel) cchat.Completer {
return Completer{ch}
}
// CompleteMessage implements message input completion capability for Discord.
// This method supports user mentions, channel mentions and emojis.
//
// For the individual implementations, refer to channel_completion.go.
func (ch Completer) Complete(words []string, i int64) (entries []cchat.CompletionEntry) {
var word = words[i]
// Word should have at least a character for the char check.
if len(word) < 1 {
return
}
switch word[0] {
case '@':
return ch.completeMentions(word[1:])
case '#':
return ch.completeChannels(word[1:])
case ':':
return ch.completeEmojis(word[1:])
}
return
}
func completionUser(s *state.Instance, u discord.User, g *discord.Guild) cchat.CompletionEntry {
if g != nil {
m, err := s.Store.Member(g.ID, u.ID)
if err == nil {
return cchat.CompletionEntry{
Raw: u.Mention(),
Text: message.RenderMemberName(*m, *g, s),
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
IconURL: u.AvatarURL(),
}
}
}
return cchat.CompletionEntry{
Raw: u.Mention(),
Text: text.Rich{Content: u.Username},
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
IconURL: u.AvatarURL(),
}
}
func (ch Completer) completeMentions(word string) (entries []cchat.CompletionEntry) {
// If there is no input, then we should grab the latest messages.
if word == "" {
msgs, _ := ch.State.Store.Messages(ch.ID)
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
// Keep track of the number of authors.
// TODO: fix excess allocations
var authors = make(map[discord.UserID]struct{}, MaxCompletion)
for _, msg := range msgs {
// If we've already added the author into the list, then skip.
if _, ok := authors[msg.Author.ID]; ok {
continue
}
// Record the current author and add the entry to the list.
authors[msg.Author.ID] = struct{}{}
entries = append(entries, completionUser(ch.State, msg.Author, g))
if len(entries) >= MaxCompletion {
return
}
}
return
}
// Lower-case everything for a case-insensitive match. contains() should
// do the rest.
var match = strings.ToLower(word)
// If we're not in a guild, then we can check the list of recipients.
if !ch.GuildID.IsValid() {
c, err := ch.State.Store.Channel(ch.ID)
if err != nil {
return
}
for _, u := range c.DMRecipients {
if contains(match, u.Username) {
entries = append(entries, cchat.CompletionEntry{
Raw: u.Mention(),
Text: text.Rich{Content: u.Username},
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
IconURL: u.AvatarURL(),
})
if len(entries) >= MaxCompletion {
return
}
}
}
return
}
// If we're in a guild, then we should search for (all) members.
m, merr := ch.State.Store.Members(ch.GuildID)
g, gerr := ch.State.Store.Guild(ch.GuildID)
if merr != nil || gerr != nil {
return
}
// If we couldn't find any members, then we can request Discord to
// search for them.
if len(m) == 0 {
ch.State.MemberState.SearchMember(ch.GuildID, word)
return
}
for _, mem := range m {
if contains(match, mem.User.Username, mem.Nick) {
entries = append(entries, cchat.CompletionEntry{
Raw: mem.User.Mention(),
Text: message.RenderMemberName(mem, *g, ch.State),
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
IconURL: mem.User.AvatarURL(),
})
if len(entries) >= MaxCompletion {
return
}
}
}
return
}
func (ch Completer) completeChannels(word string) (entries []cchat.CompletionEntry) {
// Ignore if empty word.
if word == "" {
return
}
// Ignore if we're not in a guild.
if !ch.GuildID.IsValid() {
return
}
c, err := ch.State.Store.Channels(ch.GuildID)
if err != nil {
return
}
var match = strings.ToLower(word)
for _, channel := range c {
if !contains(match, channel.Name) {
continue
}
var category string
if channel.CategoryID.IsValid() {
if c, _ := ch.State.Store.Channel(channel.CategoryID); c != nil {
category = c.Name
}
}
entries = append(entries, cchat.CompletionEntry{
Raw: channel.Mention(),
Text: text.Rich{Content: "#" + channel.Name},
Secondary: text.Rich{Content: category},
})
if len(entries) >= MaxCompletion {
return
}
}
return
}
func (ch Completer) completeEmojis(word string) (entries []cchat.CompletionEntry) {
// Ignore if empty word.
if word == "" {
return
}
e, err := ch.State.EmojiState.Get(ch.GuildID)
if err != nil {
return
}
var match = strings.ToLower(word)
for _, guild := range e {
for _, emoji := range guild.Emojis {
if contains(match, emoji.Name) {
entries = append(entries, cchat.CompletionEntry{
Raw: emoji.String(),
Text: text.Rich{Content: ":" + emoji.Name + ":"},
Secondary: text.Rich{Content: guild.Name},
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
Image: true,
})
if len(entries) >= MaxCompletion {
return
}
}
}
}
return
}
func contains(contains string, strs ...string) bool {
for _, str := range strs {
if strings.Contains(strings.ToLower(str), contains) {
return true
}
}
return false
}