Fixed out-of-bound panic; added channel commander

This commit is contained in:
diamondburned 2020-10-14 13:22:11 -07:00
parent 1907986ceb
commit aad603593f
15 changed files with 613 additions and 238 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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
}

View File

@ -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]
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -22,7 +22,7 @@ import (
)
type Messenger struct {
*empty.Messenger
empty.Messenger
*shared.Channel
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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(),
}
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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