diff --git a/go.mod b/go.mod index aba9ef0..8abd893 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/diamondburned/arikawa v1.3.0 - github.com/diamondburned/cchat v0.3.1 + github.com/diamondburned/cchat v0.3.5 github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58 github.com/dustin/go-humanize v1.0.0 github.com/go-test/deep v1.0.7 diff --git a/go.sum b/go.sum index 0061ebf..45fbf36 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,10 @@ github.com/diamondburned/cchat v0.3.0 h1:xC8Y+/nwsVhc4a7i7R+4n0JczOnFSA2Gmj6Bz/p github.com/diamondburned/cchat v0.3.0/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.1 h1:7NbVjT50dmLxcHPm+eDFF5jcaZw3t/9IdSEkZ/md1Rg= github.com/diamondburned/cchat v0.3.1/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= +github.com/diamondburned/cchat v0.3.4 h1:9JvcIrmy00cZMc2acfTSARTEzdtrSOqeIz/iYjHOgl4= +github.com/diamondburned/cchat v0.3.4/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= +github.com/diamondburned/cchat v0.3.5 h1:6rweOEmFLJUlrC98sLFwUUp9H+GWhVgtEqW5suF+J/o= +github.com/diamondburned/cchat v0.3.5/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= 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/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M= diff --git a/internal/discord/channel/channel.go b/internal/discord/channel/channel.go index f470333..033a38d 100644 --- a/internal/discord/channel/channel.go +++ b/internal/discord/channel/channel.go @@ -12,8 +12,10 @@ import ( ) type Channel struct { - *empty.Server + empty.Server + *shared.Channel + commander cchat.Commander } var _ cchat.Server = (*Channel)(nil) @@ -25,30 +27,16 @@ func New(s *state.Instance, ch discord.Channel) (cchat.Server, error) { return nil, errors.Wrap(err, "Failed to get permission") } - return Channel{ - Channel: &shared.Channel{ - ID: ch.ID, - GuildID: ch.GuildID, - State: s, - }, - }, nil -} - -// self does not do IO. -func (ch Channel) self() (*discord.Channel, error) { - return ch.State.Store.Channel(ch.Channel.ID) -} - -// messages does not do IO. -func (ch Channel) messages() ([]discord.Message, error) { - return ch.State.Store.Messages(ch.Channel.ID) -} - -func (ch Channel) guild() (*discord.Guild, error) { - if ch.GuildID.IsValid() { - return ch.State.Store.Guild(ch.GuildID) + sharedCh := &shared.Channel{ + ID: ch.ID, + GuildID: ch.GuildID, + State: s, } - return nil, errors.New("channel not in a guild") + + return Channel{ + Channel: sharedCh, + commander: NewCommander(sharedCh), + }, nil } func (ch Channel) ID() cchat.ID { @@ -56,7 +44,7 @@ func (ch Channel) ID() cchat.ID { } func (ch Channel) Name() text.Rich { - c, err := ch.self() + c, err := ch.Self() if err != nil { return text.Rich{Content: ch.Channel.ID.String()} } @@ -68,6 +56,10 @@ func (ch Channel) Name() text.Rich { } } +func (ch Channel) AsCommander() cchat.Commander { + return ch.commander +} + func (ch Channel) AsMessenger() cchat.Messenger { if !ch.HasPermission(discord.PermissionViewChannel) { return nil diff --git a/internal/discord/channel/commander.go b/internal/discord/channel/commander.go new file mode 100644 index 0000000..1d94c6d --- /dev/null +++ b/internal/discord/channel/commander.go @@ -0,0 +1,77 @@ +package channel + +import ( + "strings" + + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/channel/commands" + "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" + "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat/text" +) + +type Commander struct { + *shared.Channel + msgCompl complete.Completer +} + +func NewCommander(ch *shared.Channel) cchat.Commander { + return Commander{ + Channel: ch, + msgCompl: complete.Completer{ + Channel: ch, + }, + } +} + +func (ch Commander) AsCompleter() cchat.Completer { return ch } + +func (ch Commander) Run(words []string) ([]byte, error) { + return commands.World.Run(ch.Channel, words) +} + +func (ch Commander) Complete(words []string, i int64) []cchat.CompletionEntry { + if i == 0 { + commands := commands.World.Find(words[0]) + + var entries = make([]cchat.CompletionEntry, 0, len(commands)) + if strings.HasPrefix(words[0], "help") { + entries = append(entries, cchat.CompletionEntry{ + Raw: "help", + Text: text.Plain("help"), + Secondary: text.Plain("Prints the help message"), + }) + } + + for _, cmd := range commands { + entries = append(entries, cchat.CompletionEntry{ + Raw: cmd.Name, + Text: text.Plain(cmd.Name), + Secondary: text.Plain(cmd.Desc), + }) + } + + return entries + } + + cmd := commands.World.FindExact(words[0]) + if cmd == nil { + return nil + } + + name, _ := cmd.Args.At(int(i) - 1) + if name == "" { + return nil + } + + switch name { + case "mention:user": + return ch.msgCompl.CompleteMentions(words[i]) + case "mention:emoji": + return ch.msgCompl.CompleteEmojis(words[i]) + case "mention:channel": + return ch.msgCompl.CompleteChannels(words[i]) + } + + return nil +} diff --git a/internal/discord/channel/commands/arguments.go b/internal/discord/channel/commands/arguments.go new file mode 100644 index 0000000..361b3dc --- /dev/null +++ b/internal/discord/channel/commands/arguments.go @@ -0,0 +1,43 @@ +package commands + +import ( + "bytes" + "strings" +) + +type Arguments []string + +func (args Arguments) writeHelp(builder *bytes.Buffer) { + for i, arg := range args { + builder.WriteByte(' ') + + // Always treat the last argument as a must. + if i == len(args)-1 { + builder.WriteByte('<') + builder.WriteString(arg) + builder.WriteByte('>') + } else { + builder.WriteByte('[') + builder.WriteString(arg) + builder.WriteByte(']') + } + } +} + +// At returns a two-part string if i is in the list of arguments. Two empty +// strings are returned if i is out of bounds. If the argument is not a flag +// (i.e. not optional), then flag is empty, but name isn't. +func (args Arguments) At(i int) (name, flag string) { + if i >= len(args) { + return "", "" + } + + arg := args[i] + fis := strings.Fields(arg) + + if len(fis) != 2 { + return arg, "" + } + + return fis[1], fis[0] +} diff --git a/internal/discord/channel/commands/command.go b/internal/discord/channel/commands/command.go new file mode 100644 index 0000000..8c8304c --- /dev/null +++ b/internal/discord/channel/commands/command.go @@ -0,0 +1,24 @@ +package commands + +import ( + "bytes" + + "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" +) + +type Command struct { + Name string + Args Arguments + Desc string + RunFunc func(*shared.Channel, []string) ([]byte, error) // words[1:] +} + +func (cmd Command) writeHelp(builder *bytes.Buffer) { + builder.WriteString(cmd.Name) + cmd.Args.writeHelp(builder) + + if cmd.Desc != "" { + builder.WriteString("\n\t") + builder.WriteString(cmd.Desc) + } +} diff --git a/internal/discord/channel/commands/commands.go b/internal/discord/channel/commands/commands.go new file mode 100644 index 0000000..c7053a3 --- /dev/null +++ b/internal/discord/channel/commands/commands.go @@ -0,0 +1,188 @@ +package commands + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "strings" + + "github.com/diamondburned/arikawa/bot/extras/arguments" + "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/pkg/errors" +) + +type Commands []Command + +// Help renders the help text. +func (cmds Commands) Help() []byte { + var builder bytes.Buffer + for _, cmd := range cmds { + cmd.writeHelp(&builder) + builder.WriteString("\n") + } + + return builder.Bytes() +} + +// Run runs a command with the given words. It errors out if the command is not +// found. +func (cmds Commands) Run(ch *shared.Channel, words []string) ([]byte, error) { + if words[0] == "help" { + return cmds.Help(), nil + } + + cmd := cmds.FindExact(words[0]) + if cmd == nil { + return nil, fmt.Errorf("unknown command %q, refer to help", words[0]) + } + + return cmd.RunFunc(ch, words) +} + +// FindExact finds the exact command. It returns a pointer to the command +// directly in the slice if found. If not, nil is returned. +func (cmds Commands) FindExact(name string) *Command { + for i, cmd := range cmds { + if cmd.Name == name { + return &cmds[i] + } + } + return nil +} + +// Find finds commands with the given name. The searching is case insensitive. +func (cmds Commands) Find(name string) []Command { + name = strings.ToLower(name) + + var found []Command + + for _, cmd := range cmds { + if strings.HasPrefix(strings.ToLower(cmd.Name), name) { + // Micro-optimization. + if found == nil { + found = make([]Command, 1, len(cmds)) + found[0] = cmd + } else { + found = append(found, cmd) + } + } + } + + return found +} + +// World is a list of commands. +var World = Commands{ + { + Name: "send-embed", + Args: Arguments{"-t title", "-c color", "description"}, + Desc: "Send a basic embed to the current channel", + RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) { + var embed discord.Embed + var color uint // no Uint32Var + + fs := flag.NewFlagSet("send-embed", 0) + fs.SetOutput(ioutil.Discard) + fs.StringVar(&embed.Title, "t", "", "Embed title") + fs.UintVar(&color, "c", 0xFFFFFF, "Embed color") + + if err := fs.Parse(argv); err != nil { + return nil, err + } + + embed.Color = discord.Color(color) + + m, err := ch.State.SendEmbed(ch.ID, embed) + if err != nil { + return nil, errors.Wrap(err, "failed to send embed") + } + + return bprintf("Message %d sent at %v.", m.ID, m.Timestamp.Time()), nil + }, + }, + { + Name: "info", + Desc: "Print information as JSON", + RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) { + channel, err := ch.State.Channel(ch.ID) + if err != nil { + return nil, errors.Wrap(err, "failed to get channel") + } + + b, err := json.MarshalIndent(channel, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal to JSON") + } + + return b, nil + }, + }, + { + Name: "list-channels", + Desc: "Print all channels of this guild and their topics", + RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) { + channels, err := ch.State.Channels(ch.GuildID) + if err != nil { + return nil, errors.Wrap(err, "failed to get channels") + } + + var buf bytes.Buffer + for _, ch := range channels { + fmt.Fprintf(&buf, "#%s (NSFW %t): %s\n", ch.Name, ch.NSFW, ch.Topic) + } + + return buf.Bytes(), nil + }, + }, + { + Name: "presence", + Args: Arguments{"mention:user"}, + Desc: "Print JSON of a member/user's presence state", + RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) { + if err := assertArgc(argv, 1); err != nil { + return nil, err + } + + var user arguments.UserMention + if err := user.Parse(argv[0]); err != nil { + return nil, err + } + + p, err := ch.State.Presence(ch.GuildID, user.ID()) + if err != nil { + return nil, err + } + + return renderJSON(p) + }, + }, +} + +func assertArgc(argv []string, argc int) error { + switch { + case len(argv) > argc: + return errors.New("too many arguments") + case len(argv) < argc: + return errors.New("too few arguments") + default: + return nil + } +} + +func renderJSON(v interface{}) ([]byte, error) { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal to JSON") + } + return b, nil +} + +// bprintf is sprintf but for byte slices. +func bprintf(f string, v ...interface{}) []byte { + var buf bytes.Buffer + fmt.Fprintf(&buf, f, v...) + return buf.Bytes() +} diff --git a/internal/discord/channel/message/message.go b/internal/discord/channel/message/message.go index f5e5f5f..0b4d009 100644 --- a/internal/discord/channel/message/message.go +++ b/internal/discord/channel/message/message.go @@ -22,7 +22,7 @@ import ( ) type Messenger struct { - *empty.Messenger + empty.Messenger *shared.Channel } diff --git a/internal/discord/channel/message/send/complete/channel.go b/internal/discord/channel/message/send/complete/channel.go new file mode 100644 index 0000000..3a4b330 --- /dev/null +++ b/internal/discord/channel/message/send/complete/channel.go @@ -0,0 +1,52 @@ +package complete + +import ( + "strings" + + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat/text" +) + +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 +} diff --git a/internal/discord/channel/message/send/complete/completer.go b/internal/discord/channel/message/send/complete/completer.go index 9a2fc3b..1c127d3 100644 --- a/internal/discord/channel/message/send/complete/completer.go +++ b/internal/discord/channel/message/send/complete/completer.go @@ -3,13 +3,8 @@ 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 { @@ -26,209 +21,23 @@ func New(ch *shared.Channel) cchat.Completer { // 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) { +func (ch Completer) Complete(words []string, i int64) []cchat.CompletionEntry { var word = words[i] // Word should have at least a character for the char check. if len(word) < 1 { - return + return nil } switch word[0] { case '@': - return ch.completeMentions(word[1:]) + return ch.CompleteMentions(word[1:]) case '#': - return ch.completeChannels(word[1:]) + return ch.CompleteChannels(word[1:]) case ':': - return ch.completeEmojis(word[1:]) + 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 + return nil } func contains(contains string, strs ...string) bool { diff --git a/internal/discord/channel/message/send/complete/emoji.go b/internal/discord/channel/message/send/complete/emoji.go new file mode 100644 index 0000000..76bcf79 --- /dev/null +++ b/internal/discord/channel/message/send/complete/emoji.go @@ -0,0 +1,42 @@ +package complete + +import ( + "strings" + + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/urlutils" + "github.com/diamondburned/cchat/text" +) + +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 +} diff --git a/internal/discord/channel/message/send/complete/mention.go b/internal/discord/channel/message/send/complete/mention.go new file mode 100644 index 0000000..b6a7b42 --- /dev/null +++ b/internal/discord/channel/message/send/complete/mention.go @@ -0,0 +1,120 @@ +package complete + +import ( + "strings" + + "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/message" + "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/text" +) + +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 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(), + } +} diff --git a/internal/discord/folder/folder.go b/internal/discord/folder/folder.go index ad848b8..37b409f 100644 --- a/internal/discord/folder/folder.go +++ b/internal/discord/folder/folder.go @@ -25,7 +25,8 @@ func New(s *state.Instance, gf gateway.GuildFolder) cchat.Server { var names = make([]string, 0, len(gf.GuildIDs)) for _, id := range gf.GuildIDs { - if g, _ := s.Store.Guild(id); g != nil { + g, err := s.Store.Guild(id) + if err == nil { names = append(names, g.Name) } } diff --git a/internal/discord/session/session.go b/internal/discord/session/session.go index 34bdb96..49d00e7 100644 --- a/internal/discord/session/session.go +++ b/internal/discord/session/session.go @@ -19,7 +19,7 @@ import ( var ErrMFA = session.ErrMFA type Session struct { - *empty.Session + empty.Session *state.Instance } @@ -90,7 +90,7 @@ func (s *Session) servers(container cchat.ServersContainer) error { for _, guildFolder := range s.Ready.Settings.GuildFolders { // TODO: correct. switch { - case guildFolder.ID > 0: + case guildFolder.ID != 0: fallthrough case len(guildFolder.GuildIDs) > 1: toplevels = append(toplevels, folder.New(s.Instance, guildFolder)) diff --git a/internal/segments/mention/user.go b/internal/segments/mention/user.go index b4d45f0..2c99047 100644 --- a/internal/segments/mention/user.go +++ b/internal/segments/mention/user.go @@ -55,12 +55,15 @@ func (m NameSegment) Bounds() (start, end int) { return m.start, m.end } -func (m NameSegment) AsMentioner() text.Mentioner { - return &m.um -} +func (m NameSegment) AsMentioner() text.Mentioner { return &m.um } +func (m NameSegment) AsAvatarer() text.Avatarer { return &m.um } -func (m NameSegment) AsAvatarer() text.Avatarer { - return &m.um +// AsColorer only returns User if the user actually has a colored role. +func (m NameSegment) AsColorer() text.Colorer { + if m.um.HasColor() { + return &m.um + } + return nil } type User struct { @@ -102,31 +105,51 @@ func NewUser(state state.Store, guild discord.GuildID, guser discord.GuildUser) } } -func (um *User) Color() uint32 { +// HasColor returns true if the current user has a color. +func (um User) HasColor() bool { + // We don't have any member color if we have neither the member nor guild. + if !um.Guild.ID.IsValid() || !um.Member.User.ID.IsValid() { + return false + } + + g, err := um.state.Guild(um.Guild.ID) + if err != nil { + return false + } + + return discord.MemberColor(*g, um.Member) > 0 +} + +func (um User) Color() uint32 { g, err := um.state.Guild(um.Guild.ID) if err != nil { return colored.Blurple } - return text.SolidColor(discord.MemberColor(*g, um.Member).Uint32()) + var color = discord.MemberColor(*g, um.Member) + if color == 0 { + return colored.Blurple + } + + return text.SolidColor(color.Uint32()) } -func (um *User) AvatarSize() int { +func (um User) AvatarSize() int { return 96 } -func (um *User) AvatarText() string { +func (um User) AvatarText() string { if um.Member.Nick != "" { return um.Member.Nick } return um.Member.User.Username } -func (um *User) Avatar() (url string) { +func (um User) Avatar() (url string) { return um.Member.User.AvatarURL() } -func (um *User) MentionInfo() text.Rich { +func (um User) MentionInfo() text.Rich { var content bytes.Buffer var segment text.Rich