mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-15 17:19:10 +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()
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ go_library(
|
|||
importpath = "github.com/roleypoly/roleypoly/src/discord-bot",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/common",
|
||||
"//src/common/bot",
|
||||
"//src/common/version",
|
||||
"//src/discord-bot/internal/strings",
|
||||
"@com_github_bwmarrin_discordgo//:discordgo",
|
||||
|
|
|
@ -1,47 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/lampjaw/discordclient"
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/common/version"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
var (
|
||||
botToken = os.Getenv("DISCORD_BOT_TOKEN")
|
||||
botClientID = os.Getenv("DISCORD_CLIENT_ID")
|
||||
rootUsers = strings.Split(os.Getenv("ROOT_USERS"), ",")
|
||||
allowedBots = strings.Split(os.Getenv("ALLOWED_BOTS"), ",")
|
||||
appURL = os.Getenv("PUBLIC_URL")
|
||||
|
||||
selfMention = regexp.MustCompile("<@!?" + botClientID + ">")
|
||||
botToken = common.Getenv("DISCORD_BOT_TOKEN").String()
|
||||
botClientID = common.Getenv("DISCORD_CLIENT_ID").String()
|
||||
rootUsers = common.Getenv("ROOT_USERS").StringSlice()
|
||||
allowedBots = common.Getenv("ALLOWED_BOTS").StringSlice()
|
||||
appURL = common.Getenv("PUBLIC_URL").String()
|
||||
selfMention = bot.MentionMatcher(botClientID)
|
||||
)
|
||||
|
||||
func main() {
|
||||
klog.Info(version.StartupInfo("discord-bot"))
|
||||
|
||||
discordClient := discordclient.NewDiscordClient(botToken, rootUsers[0], botClientID)
|
||||
discordClient.GatewayIntents = discordgo.IntentsGuildMessages
|
||||
|
||||
messageChannel, err := discordClient.Listen(-1)
|
||||
err := bot.ScaffoldBot(bot.BotScaffolding{
|
||||
RootUsers: rootUsers,
|
||||
AllowBots: true,
|
||||
BotClientID: botClientID,
|
||||
BotToken: botToken,
|
||||
GatewayIntents: discordgo.IntentsGuildMessages,
|
||||
Handler: handle,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
|
||||
defer awaitExit()
|
||||
|
||||
l := listener{
|
||||
client: discordClient,
|
||||
}
|
||||
|
||||
go l.processMessages(messageChannel)
|
||||
}
|
||||
|
||||
func isBotAllowlisted(userID string) bool {
|
||||
|
@ -53,15 +43,3 @@ func isBotAllowlisted(userID string) bool {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func awaitExit() {
|
||||
syscallExit := make(chan os.Signal, 1)
|
||||
signal.Notify(
|
||||
syscallExit,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
os.Interrupt,
|
||||
os.Kill,
|
||||
)
|
||||
<-syscallExit
|
||||
}
|
||||
|
|
|
@ -1,28 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lampjaw/discordclient"
|
||||
"k8s.io/klog"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/discord-bot/internal/strings"
|
||||
)
|
||||
|
||||
type listener struct {
|
||||
client *discordclient.DiscordClient
|
||||
}
|
||||
|
||||
func (l *listener) processMessages(messageChannel <-chan discordclient.Message) {
|
||||
for {
|
||||
go l.handle(<-messageChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *listener) handle(message discordclient.Message) {
|
||||
// Only if it's a message create
|
||||
if message.Type() != discordclient.MessageTypeCreate {
|
||||
return
|
||||
}
|
||||
|
||||
func handle(ctx context.Context, message discordclient.Message) {
|
||||
// Only if it's an allowed bot
|
||||
if message.IsBot() && !isBotAllowlisted(message.UserID()) {
|
||||
return
|
||||
|
@ -33,21 +21,17 @@ func (l *listener) handle(message discordclient.Message) {
|
|||
return
|
||||
}
|
||||
|
||||
l.defaultResponse(message)
|
||||
bot.ReplyToMessage(ctx, defaultResponse(message))
|
||||
}
|
||||
|
||||
func (l *listener) defaultResponse(message discordclient.Message) {
|
||||
channel := message.Channel()
|
||||
func defaultResponse(message discordclient.Message) string {
|
||||
guild, err := message.ResolveGuildID()
|
||||
if err != nil {
|
||||
klog.Warning("failed to fetch guild, ", err)
|
||||
}
|
||||
|
||||
l.client.SendMessage(
|
||||
channel,
|
||||
strings.Render(
|
||||
strings.MentionResponse,
|
||||
strings.MentionResponseData{GuildID: guild, AppURL: appURL},
|
||||
),
|
||||
return strings.Render(
|
||||
strings.MentionResponse,
|
||||
strings.MentionResponseData{GuildID: guild, AppURL: appURL},
|
||||
)
|
||||
}
|
||||
|
|
41
src/midori/BUILD.bazel
Normal file
41
src/midori/BUILD.bazel
Normal file
|
@ -0,0 +1,41 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
load("@io_bazel_rules_docker//go:image.bzl", "go_image")
|
||||
load("@io_bazel_rules_docker//container:container.bzl", "container_push")
|
||||
|
||||
go_library(
|
||||
name = "midori_lib",
|
||||
srcs = ["midori.go"],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/midori",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/common/bot",
|
||||
"//src/common/version",
|
||||
"//src/midori/internal/commands/gh",
|
||||
"//src/midori/internal/commands/tfc",
|
||||
"//src/midori/internal/config",
|
||||
"@com_github_bwmarrin_discordgo//:discordgo",
|
||||
"@com_github_joho_godotenv//autoload",
|
||||
"@io_k8s_klog//:klog",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "midori",
|
||||
embed = [":midori_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_image(
|
||||
name = "image",
|
||||
embed = [":midori_lib"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
container_push(
|
||||
name = "+publish",
|
||||
format = "Docker",
|
||||
image = ":image",
|
||||
registry = "docker.pkg.github.com",
|
||||
repository = "roleypoly/roleypoly/midori",
|
||||
tag = "{STABLE_GIT_BRANCH}",
|
||||
)
|
5
src/midori/README.md
Normal file
5
src/midori/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Midori
|
||||
|
||||
Midori is a GitOps/GitHub Actions/Terraform Cloud helper bot. In it's simplest form, it is a hotline into GitHub Actions and Terraform Cloud to run and act upon the actual pipelines and such.
|
||||
|
||||
This is **not** a public bot for very obvious reasons. You may host it yourself, but it is not a required part of a Roleypoly deployment.
|
16
src/midori/internal/commands/gh/BUILD.bazel
Normal file
16
src/midori/internal/commands/gh/BUILD.bazel
Normal file
|
@ -0,0 +1,16 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "gh",
|
||||
srcs = ["gh.go"],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/midori/internal/commands/gh",
|
||||
visibility = ["//src/midori:__subpackages__"],
|
||||
deps = [
|
||||
"//src/common/bot",
|
||||
"//src/midori/internal/commands/helpers",
|
||||
"//src/midori/internal/config",
|
||||
"@com_github_google_go_github_v32//github:go_default_library",
|
||||
"@com_github_lampjaw_discordclient//:discordclient",
|
||||
"@org_golang_x_oauth2//:oauth2",
|
||||
],
|
||||
)
|
51
src/midori/internal/commands/gh/gh.go
Normal file
51
src/midori/internal/commands/gh/gh.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package gh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/lampjaw/discordclient"
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/commands/helpers"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/config"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type GitHubCommands struct {
|
||||
ghClient *github.Client
|
||||
}
|
||||
|
||||
func NewGitHubCommands() GitHubCommands {
|
||||
ctx := context.Background()
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: config.GitHubToken},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
ghClient := github.NewClient(tc)
|
||||
|
||||
return GitHubCommands{
|
||||
ghClient: ghClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (ghc GitHubCommands) CommandGroup() bot.CommandGroup {
|
||||
return bot.CommandGroup{
|
||||
{
|
||||
CommandName: "dispatch",
|
||||
Handler: helpers.MustHaveElevatedPermissions(ghc.dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ghc GitHubCommands) dispatch(ctx context.Context, message discordclient.Message) {
|
||||
tokens := bot.Tokenize(message)
|
||||
repo, webhookName := tokens[0], tokens[1]
|
||||
payload := json.RawMessage(strings.Join(tokens[2:], " "))
|
||||
|
||||
ghc.ghClient.Repositories.Dispatch(ctx, config.GitHubOrg, repo, github.DispatchRequestOptions{
|
||||
EventType: webhookName,
|
||||
ClientPayload: &payload,
|
||||
})
|
||||
}
|
14
src/midori/internal/commands/helpers/BUILD.bazel
Normal file
14
src/midori/internal/commands/helpers/BUILD.bazel
Normal file
|
@ -0,0 +1,14 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "helpers",
|
||||
srcs = ["permissions.go"],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/midori/internal/commands/helpers",
|
||||
visibility = ["//src/midori:__subpackages__"],
|
||||
deps = [
|
||||
"//src/common",
|
||||
"//src/common/bot",
|
||||
"//src/midori/internal/config",
|
||||
"@com_github_lampjaw_discordclient//:discordclient",
|
||||
],
|
||||
)
|
27
src/midori/internal/commands/helpers/permissions.go
Normal file
27
src/midori/internal/commands/helpers/permissions.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lampjaw/discordclient"
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/config"
|
||||
)
|
||||
|
||||
// MustHaveElevatedPermissions ensures a command has either Developer or Root role conditions.
|
||||
func MustHaveElevatedPermissions(next bot.HandlerFunc) bot.HandlerFunc {
|
||||
return func(ctx context.Context, message discordclient.Message) {
|
||||
if common.FindString(message.UserID(), config.RootUsers) || common.FindString(message.UserID(), config.AllowlistedUsers) {
|
||||
next(ctx, message)
|
||||
return
|
||||
}
|
||||
|
||||
NoPermissionsResponse(ctx, message)
|
||||
}
|
||||
}
|
||||
|
||||
// NoPermissionsResponse responds with a simple message that shows why the command failed.
|
||||
func NoPermissionsResponse(ctx context.Context, message discordclient.Message) {
|
||||
bot.ReplyToMessage(ctx, "⛔ You do not have elevated permissions.")
|
||||
}
|
13
src/midori/internal/commands/tfc/BUILD.bazel
Normal file
13
src/midori/internal/commands/tfc/BUILD.bazel
Normal file
|
@ -0,0 +1,13 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "tfc",
|
||||
srcs = ["tfc.go"],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/midori/internal/commands/tfc",
|
||||
visibility = ["//src/midori:__subpackages__"],
|
||||
deps = [
|
||||
"//src/common/bot",
|
||||
"//src/midori/internal/commands/helpers",
|
||||
"@com_github_lampjaw_discordclient//:discordclient",
|
||||
],
|
||||
)
|
25
src/midori/internal/commands/tfc/tfc.go
Normal file
25
src/midori/internal/commands/tfc/tfc.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lampjaw/discordclient"
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/commands/helpers"
|
||||
)
|
||||
|
||||
type TerraformCloudCommands struct {
|
||||
}
|
||||
|
||||
func (tfcc TerraformCloudCommands) CommandGroup() bot.CommandGroup {
|
||||
return bot.CommandGroup{
|
||||
{
|
||||
CommandName: "dispatch",
|
||||
Handler: helpers.MustHaveElevatedPermissions(tfcc.dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tfcc TerraformCloudCommands) dispatch(ctx context.Context, message discordclient.Message) {
|
||||
bot.ReplyToMessage(ctx, "Ok!")
|
||||
}
|
9
src/midori/internal/config/BUILD.bazel
Normal file
9
src/midori/internal/config/BUILD.bazel
Normal file
|
@ -0,0 +1,9 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "config",
|
||||
srcs = ["config.go"],
|
||||
importpath = "github.com/roleypoly/roleypoly/src/midori/internal/config",
|
||||
visibility = ["//src/midori:__subpackages__"],
|
||||
deps = ["//src/common"],
|
||||
)
|
15
src/midori/internal/config/config.go
Normal file
15
src/midori/internal/config/config.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
)
|
||||
|
||||
var (
|
||||
BotToken = common.Getenv("MIDORI_BOT_TOKEN").String()
|
||||
ClientID = common.Getenv("MIDORI_CLIENT_ID").String()
|
||||
AllowlistedUsers = common.Getenv("MIDORI_DEVELOPERS").StringSlice()
|
||||
CommandPrefix = common.Getenv("MIDORI_PREFIX_OVERRIDE", "midori").String()
|
||||
RootUsers = common.Getenv("ROOT_USERS").StringSlice()
|
||||
GitHubOrg = common.Getenv("MIDORI_GITHUB_ORG").String()
|
||||
GitHubToken = common.Getenv("MIDORI_GITHUB_TOKEN").String()
|
||||
)
|
33
src/midori/midori.go
Normal file
33
src/midori/midori.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"k8s.io/klog"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common/bot"
|
||||
"github.com/roleypoly/roleypoly/src/common/version"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/commands/gh"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/commands/tfc"
|
||||
"github.com/roleypoly/roleypoly/src/midori/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
klog.Info(version.StartupInfo("midori"))
|
||||
|
||||
mux := bot.NewCommandMux(bot.MentionMatcher(config.ClientID))
|
||||
mux.RegisterCommandGroup("gh", gh.GitHubCommands{}.CommandGroup())
|
||||
mux.RegisterCommandGroup("tfc", tfc.TerraformCloudCommands{}.CommandGroup())
|
||||
|
||||
err := bot.ScaffoldBot(bot.BotScaffolding{
|
||||
AllowBots: false,
|
||||
BotToken: config.BotToken,
|
||||
BotClientID: config.ClientID,
|
||||
RootUsers: config.RootUsers,
|
||||
GatewayIntents: discordgo.IntentsGuildMessages,
|
||||
Handler: mux.Handler,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue