Fixed out-of-bound panic; added channel commander
This commit is contained in:
parent
1907986ceb
commit
aad603593f
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -22,7 +22,7 @@ import (
|
|||
)
|
||||
|
||||
type Messenger struct {
|
||||
*empty.Messenger
|
||||
empty.Messenger
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue