mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-16 01:29:09 +00:00
feat: add midori skeleton, bot scaffolding, CD seeds
This commit is contained in:
parent
7c861b37ca
commit
49d44df231
30 changed files with 854 additions and 123 deletions
21
src/common/BUILD.bazel
Normal file
21
src/common/BUILD.bazel
Normal file
|
@ -0,0 +1,21 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "common",
|
||||
srcs = [
|
||||
"await-exit.go",
|
||||
"envconfig.go",
|
||||
"finders.go",
|
||||
],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/common",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "common_test",
|
||||
srcs = [
|
||||
"envconfig_test.go",
|
||||
"finders_test.go",
|
||||
],
|
||||
embed = [":common"],
|
||||
)
|
19
src/common/await-exit.go
Normal file
19
src/common/await-exit.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func AwaitExit() {
|
||||
syscallExit := make(chan os.Signal, 1)
|
||||
signal.Notify(
|
||||
syscallExit,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
os.Interrupt,
|
||||
os.Kill,
|
||||
)
|
||||
<-syscallExit
|
||||
}
|
18
src/common/bot/BUILD.bazel
Normal file
18
src/common/bot/BUILD.bazel
Normal file
|
@ -0,0 +1,18 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "bot",
|
||||
srcs = [
|
||||
"commandmux.go",
|
||||
"scaffolding.go",
|
||||
],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/common/bot",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/common",
|
||||
"@com_github_bwmarrin_discordgo//:discordgo",
|
||||
"@com_github_dghubble_trie//:trie",
|
||||
"@com_github_lampjaw_discordclient//:discordclient",
|
||||
"@io_k8s_klog//:klog",
|
||||
],
|
||||
)
|
87
src/common/bot/commandmux.go
Normal file
87
src/common/bot/commandmux.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/dghubble/trie"
|
||||
"github.com/lampjaw/discordclient"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
type CommandMux struct {
|
||||
commandTrie *trie.PathTrie
|
||||
matcher *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewCommandMux(matcher *regexp.Regexp) CommandMux {
|
||||
return CommandMux{
|
||||
commandTrie: trie.NewPathTrie(),
|
||||
matcher: matcher,
|
||||
}
|
||||
}
|
||||
|
||||
type CommandGroup []Command
|
||||
|
||||
func (cg CommandGroup) RegisterCommands(commandTrie *trie.PathTrie, prefix string) {
|
||||
for _, command := range cg {
|
||||
commandTrie.Put(prefix+"/"+command.CommandName, command.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
CommandName string
|
||||
Handler HandlerFunc
|
||||
}
|
||||
|
||||
func (c CommandMux) RegisterCommandGroup(prefix string, group CommandGroup) {
|
||||
group.RegisterCommands(c.commandTrie, prefix)
|
||||
}
|
||||
|
||||
func (c CommandMux) Handler(ctx context.Context, message discordclient.Message) {
|
||||
if !c.matches(message) {
|
||||
return
|
||||
}
|
||||
|
||||
key := c.commandKeyFromMessage(message)
|
||||
|
||||
command := c.commandTrie.Get(key)
|
||||
if command == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handlerFn, ok := command.(HandlerFunc)
|
||||
if !ok {
|
||||
klog.Warning("CommandMux.Handler: " + key + " handler was not HandlerFunc")
|
||||
return
|
||||
}
|
||||
|
||||
c.logCommandRun(key, message)
|
||||
handlerFn(ctx, message)
|
||||
}
|
||||
|
||||
func (c CommandMux) commandKeyFromMessage(message discordclient.Message) string {
|
||||
commandParts := strings.Split(message.RawMessage(), " ")[1:]
|
||||
return commandParts[0] + "/" + commandParts[1]
|
||||
}
|
||||
|
||||
func (c CommandMux) matches(message discordclient.Message) bool {
|
||||
return c.matcher.MatchString(message.RawMessage())
|
||||
}
|
||||
|
||||
func (c CommandMux) logCommandRun(key string, message discordclient.Message) {
|
||||
klog.Info("CommandMux: " + key + " by " + message.UserName() + " <@" + message.UserID() + ">")
|
||||
}
|
||||
|
||||
func PrefixMatcher(prefix string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`^` + prefix)
|
||||
}
|
||||
|
||||
func MentionMatcher(userID string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`<@!?` + userID + ">")
|
||||
}
|
||||
|
||||
func Tokenize(message discordclient.Message) []string {
|
||||
return strings.SplitAfterN(message.RawMessage(), " ", 3)
|
||||
}
|
101
src/common/bot/scaffolding.go
Normal file
101
src/common/bot/scaffolding.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/lampjaw/discordclient"
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
)
|
||||
|
||||
type HandlerFunc func(context.Context, discordclient.Message)
|
||||
|
||||
type BotScaffolding struct {
|
||||
BotToken string
|
||||
BotClientID string
|
||||
RootUsers []string
|
||||
AllowBots bool
|
||||
AllowEdits bool
|
||||
AllowDeletes bool
|
||||
GatewayIntents discordgo.Intent
|
||||
Handler HandlerFunc
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
DiscordClient *discordclient.DiscordClient
|
||||
config *BotScaffolding
|
||||
handler HandlerFunc
|
||||
}
|
||||
|
||||
type ctxKey string
|
||||
|
||||
var ListenerCtxKey ctxKey = "listener"
|
||||
var MessageCtxKey ctxKey = "message"
|
||||
|
||||
func ScaffoldBot(config BotScaffolding) error {
|
||||
discordClient := discordclient.NewDiscordClient(config.BotToken, config.RootUsers[0], config.BotClientID)
|
||||
discordClient.GatewayIntents = config.GatewayIntents
|
||||
discordClient.AllowBots = config.AllowBots
|
||||
|
||||
messageChannel, err := discordClient.Listen(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer common.AwaitExit()
|
||||
|
||||
listener := &Listener{
|
||||
config: &config,
|
||||
DiscordClient: discordClient,
|
||||
handler: config.Handler,
|
||||
}
|
||||
|
||||
go listener.processMessages(messageChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) processMessages(messageChannel <-chan discordclient.Message) {
|
||||
listenerCtx := context.WithValue(context.Background(), ListenerCtxKey, l)
|
||||
for {
|
||||
message := <-messageChannel
|
||||
|
||||
if !l.config.AllowEdits && message.Type() == discordclient.MessageTypeUpdate {
|
||||
continue
|
||||
}
|
||||
|
||||
if !l.config.AllowDeletes && message.Type() == discordclient.MessageTypeDelete {
|
||||
continue
|
||||
}
|
||||
|
||||
localCtx := context.WithValue(listenerCtx, MessageCtxKey, message)
|
||||
go l.handler(localCtx, message)
|
||||
}
|
||||
}
|
||||
|
||||
// ReplyToMessage will use the message context to reply to a message.
|
||||
// Message may be one of two types:
|
||||
// - *discordgo.MessageSend
|
||||
// - string
|
||||
func ReplyToMessage(listenerCtx context.Context, message interface{}) error {
|
||||
l := listenerCtx.Value(ListenerCtxKey).(*Listener)
|
||||
m := listenerCtx.Value(MessageCtxKey).(discordclient.Message)
|
||||
channelID := m.Channel()
|
||||
|
||||
switch message.(type) {
|
||||
case *discordgo.MessageSend:
|
||||
_, err := l.DiscordClient.Session.ChannelMessageSendComplex(channelID, message.(*discordgo.MessageSend))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case string:
|
||||
return l.DiscordClient.SendMessage(channelID, message.(string))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NoOpHandlerFunc does no operations againdst a message. This can be an equivalent to nil.
|
||||
func NoOpHandlerFunc(context.Context, discordclient.Message) {
|
||||
|
||||
}
|
65
src/common/envconfig.go
Normal file
65
src/common/envconfig.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetenvValue is a holder type for Getenv to translate any Getenv strings to real types
|
||||
type GetenvValue struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func Getenv(key string, defaultValue ...string) GetenvValue {
|
||||
value := ""
|
||||
if len(defaultValue) > 0 {
|
||||
value = defaultValue[0]
|
||||
}
|
||||
|
||||
envValue := os.Getenv(key)
|
||||
if envValue != "" {
|
||||
value = envValue
|
||||
}
|
||||
|
||||
return GetenvValue{
|
||||
value: strings.TrimSpace(value),
|
||||
}
|
||||
}
|
||||
|
||||
func (g GetenvValue) String() string {
|
||||
return g.value
|
||||
}
|
||||
|
||||
func (g GetenvValue) StringSlice(optionalDelimiter ...string) []string {
|
||||
delimiter := ","
|
||||
if len(optionalDelimiter) > 0 {
|
||||
delimiter = optionalDelimiter[0]
|
||||
}
|
||||
|
||||
return strings.Split(g.value, delimiter)
|
||||
}
|
||||
|
||||
func (g GetenvValue) Bool() bool {
|
||||
lowercaseValue := strings.ToLower(g.value)
|
||||
if g.value == "1" || lowercaseValue == "true" || lowercaseValue == "yes" {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (g GetenvValue) Number() int {
|
||||
result, err := strconv.Atoi(g.value)
|
||||
|
||||
if err != nil {
|
||||
return -999999
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (g GetenvValue) JSON(target interface{}) error {
|
||||
return json.Unmarshal([]byte(g.value), target)
|
||||
}
|
123
src/common/envconfig_test.go
Normal file
123
src/common/envconfig_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package common_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
)
|
||||
|
||||
var (
|
||||
testEnv = map[string]string{
|
||||
"string": "hello world",
|
||||
"slice": "hello,world",
|
||||
"slice_no_delim": "hello world",
|
||||
"slice_set_delim": "hello|world",
|
||||
"number": "10005",
|
||||
"number_bad": "abc123",
|
||||
"bool": "true",
|
||||
"bool_bad": "truth",
|
||||
"bool_no": "false",
|
||||
"bool_number": "1",
|
||||
"json": `{"hello":"world","arr":[1,2,3]}`,
|
||||
}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
for key, value := range testEnv {
|
||||
os.Setenv("test__"+key, value)
|
||||
}
|
||||
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestEnvconfigString(t *testing.T) {
|
||||
testStr := common.Getenv("test__string").String()
|
||||
if testStr != testEnv["string"] {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigStringSlice(t *testing.T) {
|
||||
testSl := common.Getenv("test__slice").StringSlice()
|
||||
if testSl[0] != "hello" || testSl[1] != "world" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigStringSliceNoDelimeter(t *testing.T) {
|
||||
testSl := common.Getenv("test__slice_no_delim").StringSlice()
|
||||
if testSl[0] != testEnv["slice_no_delim"] {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigStringSliceSetDelimeter(t *testing.T) {
|
||||
testSl := common.Getenv("test__slice_set_delim").StringSlice("|")
|
||||
if testSl[0] != "hello" || testSl[1] != "world" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigNumber(t *testing.T) {
|
||||
testNum := common.Getenv("test__number").Number()
|
||||
if testNum != 10005 {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigNumberBad(t *testing.T) {
|
||||
testNum := common.Getenv("test__number_bad").Number()
|
||||
if testNum != -999999 {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigBool(t *testing.T) {
|
||||
testBool := common.Getenv("test__bool").Bool()
|
||||
if !testBool {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigBoolBad(t *testing.T) {
|
||||
testBool := common.Getenv("test__bool_bad").Bool()
|
||||
if testBool {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigBoolFalse(t *testing.T) {
|
||||
testBool := common.Getenv("test__bool_no").Bool()
|
||||
if testBool {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigBoolNumber(t *testing.T) {
|
||||
testBool := common.Getenv("test__bool_number").Bool()
|
||||
if !testBool {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigDefault(t *testing.T) {
|
||||
testBool := common.Getenv("test__thing_that_doesnt_exist", "yes").Bool()
|
||||
if !testBool {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
type testJSONData struct {
|
||||
Hello string `json:"hello,omitempty"`
|
||||
Arr []int `json:"arr,omitempty"`
|
||||
}
|
||||
|
||||
func TestEnvconfigJSON(t *testing.T) {
|
||||
data := testJSONData{}
|
||||
err := common.Getenv("test__json").JSON(&data)
|
||||
if err != nil || data.Hello != "world" || len(data.Arr) != 3 {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
12
src/common/finders.go
Normal file
12
src/common/finders.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package common
|
||||
|
||||
// FindString returns true if needle is in haystack
|
||||
func FindString(needle string, haystack []string) bool {
|
||||
for _, str := range haystack {
|
||||
if str == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
17
src/common/finders_test.go
Normal file
17
src/common/finders_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package common_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
)
|
||||
|
||||
func TestFindString(t *testing.T) {
|
||||
if !common.FindString("hello", []string{"hello", "world"}) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if common.FindString("foo", []string{"a", "b", "c"}) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue