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 (
|
require (
|
||||||
github.com/diamondburned/arikawa v1.3.0
|
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/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-test/deep v1.0.7
|
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.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 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.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 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.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
||||||
|
|
|
@ -12,8 +12,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
*empty.Server
|
empty.Server
|
||||||
|
|
||||||
*shared.Channel
|
*shared.Channel
|
||||||
|
commander cchat.Commander
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.Server = (*Channel)(nil)
|
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 nil, errors.Wrap(err, "Failed to get permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
return Channel{
|
sharedCh := &shared.Channel{
|
||||||
Channel: &shared.Channel{
|
ID: ch.ID,
|
||||||
ID: ch.ID,
|
GuildID: ch.GuildID,
|
||||||
GuildID: ch.GuildID,
|
State: s,
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("channel not in a guild")
|
|
||||||
|
return Channel{
|
||||||
|
Channel: sharedCh,
|
||||||
|
commander: NewCommander(sharedCh),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch Channel) ID() cchat.ID {
|
func (ch Channel) ID() cchat.ID {
|
||||||
|
@ -56,7 +44,7 @@ func (ch Channel) ID() cchat.ID {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch Channel) Name() text.Rich {
|
func (ch Channel) Name() text.Rich {
|
||||||
c, err := ch.self()
|
c, err := ch.Self()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Rich{Content: ch.Channel.ID.String()}
|
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 {
|
func (ch Channel) AsMessenger() cchat.Messenger {
|
||||||
if !ch.HasPermission(discord.PermissionViewChannel) {
|
if !ch.HasPermission(discord.PermissionViewChannel) {
|
||||||
return nil
|
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 {
|
type Messenger struct {
|
||||||
*empty.Messenger
|
empty.Messenger
|
||||||
*shared.Channel
|
*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 (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
"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 {
|
type Completer struct {
|
||||||
|
@ -26,209 +21,23 @@ func New(ch *shared.Channel) cchat.Completer {
|
||||||
// This method supports user mentions, channel mentions and emojis.
|
// This method supports user mentions, channel mentions and emojis.
|
||||||
//
|
//
|
||||||
// For the individual implementations, refer to channel_completion.go.
|
// 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]
|
var word = words[i]
|
||||||
// Word should have at least a character for the char check.
|
// Word should have at least a character for the char check.
|
||||||
if len(word) < 1 {
|
if len(word) < 1 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch word[0] {
|
switch word[0] {
|
||||||
case '@':
|
case '@':
|
||||||
return ch.completeMentions(word[1:])
|
return ch.CompleteMentions(word[1:])
|
||||||
case '#':
|
case '#':
|
||||||
return ch.completeChannels(word[1:])
|
return ch.CompleteChannels(word[1:])
|
||||||
case ':':
|
case ':':
|
||||||
return ch.completeEmojis(word[1:])
|
return ch.CompleteEmojis(word[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
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))
|
var names = make([]string, 0, len(gf.GuildIDs))
|
||||||
|
|
||||||
for _, id := range 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)
|
names = append(names, g.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
var ErrMFA = session.ErrMFA
|
var ErrMFA = session.ErrMFA
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
*empty.Session
|
empty.Session
|
||||||
*state.Instance
|
*state.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ func (s *Session) servers(container cchat.ServersContainer) error {
|
||||||
for _, guildFolder := range s.Ready.Settings.GuildFolders {
|
for _, guildFolder := range s.Ready.Settings.GuildFolders {
|
||||||
// TODO: correct.
|
// TODO: correct.
|
||||||
switch {
|
switch {
|
||||||
case guildFolder.ID > 0:
|
case guildFolder.ID != 0:
|
||||||
fallthrough
|
fallthrough
|
||||||
case len(guildFolder.GuildIDs) > 1:
|
case len(guildFolder.GuildIDs) > 1:
|
||||||
toplevels = append(toplevels, folder.New(s.Instance, guildFolder))
|
toplevels = append(toplevels, folder.New(s.Instance, guildFolder))
|
||||||
|
|
|
@ -55,12 +55,15 @@ func (m NameSegment) Bounds() (start, end int) {
|
||||||
return m.start, m.end
|
return m.start, m.end
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m NameSegment) AsMentioner() text.Mentioner {
|
func (m NameSegment) AsMentioner() text.Mentioner { return &m.um }
|
||||||
return &m.um
|
func (m NameSegment) AsAvatarer() text.Avatarer { return &m.um }
|
||||||
}
|
|
||||||
|
|
||||||
func (m NameSegment) AsAvatarer() text.Avatarer {
|
// AsColorer only returns User if the user actually has a colored role.
|
||||||
return &m.um
|
func (m NameSegment) AsColorer() text.Colorer {
|
||||||
|
if m.um.HasColor() {
|
||||||
|
return &m.um
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
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)
|
g, err := um.state.Guild(um.Guild.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return colored.Blurple
|
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
|
return 96
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) AvatarText() string {
|
func (um User) AvatarText() string {
|
||||||
if um.Member.Nick != "" {
|
if um.Member.Nick != "" {
|
||||||
return um.Member.Nick
|
return um.Member.Nick
|
||||||
}
|
}
|
||||||
return um.Member.User.Username
|
return um.Member.User.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) Avatar() (url string) {
|
func (um User) Avatar() (url string) {
|
||||||
return um.Member.User.AvatarURL()
|
return um.Member.User.AvatarURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) MentionInfo() text.Rich {
|
func (um User) MentionInfo() text.Rich {
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
var segment text.Rich
|
var segment text.Rich
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue