From 0a66a65fcf279726eca2e7d9f974edb76cac6fb6 Mon Sep 17 00:00:00 2001 From: "diamondburned (Forefront)" Date: Sun, 28 Jun 2020 19:31:32 -0700 Subject: [PATCH] Added completion; bumped cchat to v0.0.35 --- channel.go | 78 ++++++++++++++--- channel_completion.go | 189 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + message.go | 14 ++-- 5 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 channel_completion.go diff --git a/channel.go b/channel.go index c63b1ae..160023d 100644 --- a/channel.go +++ b/channel.go @@ -63,13 +63,13 @@ type Channel struct { } var ( - _ cchat.Server = (*Channel)(nil) - _ cchat.ServerMessage = (*Channel)(nil) - _ cchat.ServerMessageSender = (*Channel)(nil) - // _ cchat.ServerMessageSendCompleter = (*Channel)(nil) - _ cchat.ServerNickname = (*Channel)(nil) - _ cchat.ServerMessageEditor = (*Channel)(nil) - _ cchat.ServerMessageActioner = (*Channel)(nil) + _ cchat.Server = (*Channel)(nil) + _ cchat.ServerMessage = (*Channel)(nil) + _ cchat.ServerMessageSender = (*Channel)(nil) + _ cchat.ServerMessageSendCompleter = (*Channel)(nil) + _ cchat.ServerNickname = (*Channel)(nil) + _ cchat.ServerMessageEditor = (*Channel)(nil) + _ cchat.ServerMessageActioner = (*Channel)(nil) ) func NewChannel(s *Session, ch discord.Channel) *Channel { @@ -80,12 +80,29 @@ func NewChannel(s *Session, ch discord.Channel) *Channel { } } +// self does not do IO. +func (ch *Channel) self() (*discord.Channel, error) { + return ch.session.Store.Channel(ch.id) +} + +// messages does not do IO. +func (ch *Channel) messages() ([]discord.Message, error) { + return ch.session.Store.Messages(ch.id) +} + +func (ch *Channel) guild() (*discord.Guild, error) { + if ch.guildID.Valid() { + return ch.session.Guild(ch.guildID) + } + return nil, errors.New("channel not in a guild") +} + func (ch *Channel) ID() string { return ch.id.String() } func (ch *Channel) Name() text.Rich { - c, err := ch.session.Store.Channel(ch.id) + c, err := ch.self() if err != nil { return text.Rich{Content: ch.id.String()} } @@ -159,13 +176,13 @@ func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) ( return } - m, err := ch.session.Store.Messages(ch.id) + m, err := ch.messages() if err != nil { // TODO: log return } - g, err := ch.session.Store.Guild(c.GuildID) + g, err := ch.guild() if err != nil { return } @@ -226,6 +243,23 @@ func (ch *Channel) SendMessage(msg cchat.SendableMessage) error { return err } +// MessageEditable returns true if the given message ID belongs to the current +// user. +func (ch *Channel) MessageEditable(id string) bool { + s, err := discord.ParseSnowflake(id) + if err != nil { + return false + } + + m, err := ch.session.Store.Message(ch.id, s) + if err != nil { + return false + } + + return m.Author.ID == ch.session.userID +} + +// RawMessageContent returns the raw message content from Discord. func (ch *Channel) RawMessageContent(id string) (string, error) { s, err := discord.ParseSnowflake(id) if err != nil { @@ -240,6 +274,7 @@ func (ch *Channel) RawMessageContent(id string) (string, error) { return m.Content, nil } +// EditMessage edits the message to the given content string. func (ch *Channel) EditMessage(id, content string) error { s, err := discord.ParseSnowflake(id) if err != nil { @@ -314,12 +349,12 @@ func (ch *Channel) canManageMessages(userID discord.Snowflake) bool { // We need the guild, member and channel to calculate the permission // overrides. - g, err := ch.session.Store.Guild(ch.guildID) + g, err := ch.guild() if err != nil { return false } - c, err := ch.session.Store.Channel(ch.id) + c, err := ch.self() if err != nil { return false } @@ -335,6 +370,25 @@ func (ch *Channel) canManageMessages(userID discord.Snowflake) bool { return p.Has(discord.PermissionManageMessages) } +func (ch *Channel) CompleteMessage(words []string, i int) (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 newCancels() func(...func()) []func() { var cancels []func() return func(appended ...func()) []func() { diff --git a/channel_completion.go b/channel_completion.go new file mode 100644 index 0000000..901182f --- /dev/null +++ b/channel_completion.go @@ -0,0 +1,189 @@ +package discord + +import ( + "strings" + + "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/arikawa/state" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat/text" +) + +const MaxCompletion = 15 + +func completionUserEntry(s state.Store, u discord.User, g *discord.Guild) cchat.CompletionEntry { + if g != nil { + m, err := s.Member(g.ID, u.ID) + if err == nil { + return cchat.CompletionEntry{ + Raw: u.Mention(), + Text: RenderMemberName(*m, *g), + IconURL: u.AvatarURL(), + } + } + } + + return cchat.CompletionEntry{ + Raw: u.Mention(), + Text: text.Rich{Content: u.Username}, + IconURL: u.AvatarURL(), + } +} + +func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntry) { + // If there is no input, then we should grab the latest messages. + if word == "" { + msgs, _ := ch.messages() + g, _ := ch.guild() // nil is fine + + // Keep track of the number of authors. + // TODO: fix excess allocations + var authors = make(map[discord.Snowflake]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, completionUserEntry(ch.session.Store, msg.Author, g)) + + if len(entries) >= MaxCompletion { + return + } + } + + return + } + + // Lower-case everything for a case-insensitive match. startsWith() 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.Valid() { + c, err := ch.self() + if err == nil { + for _, u := range c.DMRecipients { + if startsWith(match, u.Username) { + entries = append(entries, cchat.CompletionEntry{ + Raw: u.Mention(), + Text: text.Rich{Content: u.Username}, + 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.session.Store.Members(ch.guildID) + g, gerr := ch.guild() + + 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.session.Members.SearchMember(ch.guildID, word) + return + } + + for _, mem := range m { + if startsWith(match, mem.User.Username, mem.Nick) { + entries = append(entries, cchat.CompletionEntry{ + Raw: mem.User.Mention(), + Text: RenderMemberName(mem, *g), + IconURL: mem.User.AvatarURL(), + }) + if len(entries) >= MaxCompletion { + return + } + } + } + + return +} + +func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntry) { + // Ignore if empty word. + if word == "" { + return + } + + // Ignore if we're not in a guild. + if !ch.guildID.Valid() { + return + } + + c, err := ch.session.State.Channels(ch.guildID) + if err != nil { + return + } + + var match = strings.ToLower(word) + + for _, ch := range c { + if startsWith(match, ch.Name) { + entries = append(entries, cchat.CompletionEntry{ + Raw: ch.Mention(), + Text: text.Rich{Content: "#" + ch.Name}, + }) + if len(entries) >= MaxCompletion { + return + } + } + } + + return +} + +func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry) { + // Ignore if empty word. + if word == "" { + return + } + + e, err := ch.session.Emoji.Get(ch.guildID) + if err != nil { + return + } + + var match = strings.ToLower(word) + + for _, guild := range e { + for _, emoji := range guild.Emojis { + if startsWith(match, emoji.Name) { + entries = append(entries, cchat.CompletionEntry{ + Raw: emoji.String(), + Text: text.Rich{Content: ":" + emoji.Name + ":"}, + IconURL: emoji.EmojiURL() + "?size=32", // small + }) + if len(entries) >= MaxCompletion { + return + } + } + } + } + + return +} + +func startsWith(contains string, strs ...string) bool { + for _, str := range strs { + if strings.HasPrefix(strings.ToLower(str), contains) { + return true + } + } + + return false +} diff --git a/go.mod b/go.mod index 2f381b8..d95a831 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/diamondburned/arikawa v0.9.5 - github.com/diamondburned/cchat v0.0.34 + github.com/diamondburned/cchat v0.0.35 github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 github.com/go-test/deep v1.0.6 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index c259887..cb890a5 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBK github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU= github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk= +github.com/diamondburned/cchat v0.0.35/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/message.go b/message.go index 09e7525..e2a4782 100644 --- a/message.go +++ b/message.go @@ -66,6 +66,14 @@ func NewUser(u discord.User) Author { } func NewGuildMember(m discord.Member, g discord.Guild) Author { + return Author{ + id: m.User.ID, + name: RenderMemberName(m, g), + avatar: m.User.AvatarURL(), + } +} + +func RenderMemberName(m discord.Member, g discord.Guild) text.Rich { var name = text.Rich{ Content: m.User.Username, } @@ -82,11 +90,7 @@ func NewGuildMember(m discord.Member, g discord.Guild) Author { } } - return Author{ - id: m.User.ID, - name: name, - avatar: m.User.AvatarURL(), - } + return name } func (a Author) ID() string {