aboutsummaryrefslogtreecommitdiff
path: root/internal/pvfm/bot/bot.go
blob: 5ed093da924502e45404c5e61e0ebbc12fb6c994 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package bot

import (
	"errors"
	"log"
	"strings"
	"sync"
	"time"

	"golang.org/x/time/rate"

	"github.com/bwmarrin/discordgo"
)

var (
	ErrRateLimitExceeded = errors.New("bot: per-command rate limit exceeded")
)

type command struct {
	aliases  []string
	verb     string
	helptext string
}

func (c *command) Verb() string {
	return c.verb
}

func (c *command) Helptext() string {
	return c.helptext
}

// Handler is the type that bot command functions need to implement. Errors
// should be returned.
type Handler func(*discordgo.Session, *discordgo.Message, []string) error

// CommandHandler is a generic interface for types that implement a bot
// command. It is akin to http.Handler, but more comprehensive.
type CommandHandler interface {
	Verb() string
	Helptext() string

	Handler(*discordgo.Session, *discordgo.Message, []string) error
	Permissions(*discordgo.Session, *discordgo.Message, []string) error
}

type basicCommand struct {
	*command
	handler     Handler
	permissions Handler
	limiter     *rate.Limiter
}

func (bc *basicCommand) Handler(s *discordgo.Session, m *discordgo.Message, parv []string) error {
	return bc.handler(s, m, parv)
}

func (bc *basicCommand) Permissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
	if !bc.limiter.Allow() {
		return ErrRateLimitExceeded
	}

	return bc.permissions(s, m, parv)
}

// The "default" command set, useful for simple bot projects.
var (
	DefaultCommandSet = NewCommandSet()
)

// Command handling errors.
var (
	ErrAlreadyExists     = errors.New("bot: command already exists")
	ErrNoSuchCommand     = errors.New("bot: no such command exists")
	ErrNoPermissions     = errors.New("bot: you do not have permissions for this command")
	ErrParvCountMismatch = errors.New("bot: parameter count mismatch")
)

// The default command prefix. Command `foo` becomes `.foo` in chat, etc.
const (
	DefaultPrefix = "."
)

// NewCommand creates an anonymous command and adds it to the default CommandSet.
func NewCommand(verb, helptext string, handler, permissions Handler) error {
	return DefaultCommandSet.Add(NewBasicCommand(verb, helptext, handler, permissions))
}

// NewBasicCommand creates a CommandHandler instance using the implementation
// functions supplied as arguments.
func NewBasicCommand(verb, helptext string, permissions, handler Handler) CommandHandler {
	return &basicCommand{
		command: &command{
			verb:     verb,
			helptext: helptext,
		},
		handler:     handler,
		permissions: permissions,
		limiter:     rate.NewLimiter(rate.Every(5*time.Second), 1),
	}
}

// CommandSet is a group of bot commands similar to an http.ServeMux.
type CommandSet struct {
	sync.Mutex
	cmds map[string]CommandHandler

	Prefix string
}

// NewCommandSet creates a new command set with the `help` command pre-loaded.
func NewCommandSet() *CommandSet {
	cs := &CommandSet{
		cmds:   map[string]CommandHandler{},
		Prefix: DefaultPrefix,
	}

	cs.AddCmd("help", "Shows help for the bot", NoPermissions, cs.help)

	return cs
}

// NoPermissions is a simple middelware function that allows all command invocations
// to pass the permissions check.
func NoPermissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
	return nil
}

// AddCmd is syntactic sugar for cs.Add(NewBasicCommand(args...))
func (cs *CommandSet) AddCmd(verb, helptext string, permissions, handler Handler) error {
	return cs.Add(NewBasicCommand(verb, helptext, permissions, handler))
}

// Add adds a single command handler to the CommandSet. This can be done at runtime
// but it is suggested to only add commands on application boot.
func (cs *CommandSet) Add(h CommandHandler) error {
	cs.Lock()
	defer cs.Unlock()

	v := strings.ToLower(h.Verb())

	if _, ok := cs.cmds[v]; ok {
		return ErrAlreadyExists
	}

	cs.cmds[v] = h

	return nil
}

// Run makes a CommandSet compatible with discordgo event dispatching.
func (cs *CommandSet) Run(s *discordgo.Session, msg *discordgo.Message) error {
	cs.Lock()
	defer cs.Unlock()

	if strings.HasPrefix(msg.Content, cs.Prefix) {
		params := strings.Fields(msg.Content)
		verb := strings.ToLower(params[0][1:])

		cmd, ok := cs.cmds[verb]
		if !ok {
			return ErrNoSuchCommand
		}

		err := cmd.Permissions(s, msg, params)
		if err != nil {
			log.Printf("Permissions error: %s: %v", msg.Author.Username, err)
			s.ChannelMessageSend(msg.ChannelID, "You don't have permissions for that, sorry.")
			return ErrNoPermissions
		}

		err = cmd.Handler(s, msg, params)
		if err != nil {
			log.Printf("command handler error: %v", err)
			s.ChannelMessageSend(msg.ChannelID, "error when running that command: "+err.Error())
			return err
		}
	}

	return nil
}