diff --git a/.gitignore b/.gitignore index d7e2efa..4c49bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .env -vyvanse diff --git a/cmd/vyvanse/.DS_Store b/cmd/vyvanse/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/cmd/vyvanse/.DS_Store differ diff --git a/cmd/vyvanse/.dockerignore b/cmd/vyvanse/.dockerignore new file mode 100644 index 0000000..dd90ea6 --- /dev/null +++ b/cmd/vyvanse/.dockerignore @@ -0,0 +1 @@ +vyvanse \ No newline at end of file diff --git a/cmd/vyvanse/dice.go b/cmd/vyvanse/dice.go new file mode 100644 index 0000000..e5be897 --- /dev/null +++ b/cmd/vyvanse/dice.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "errors" + + "github.com/bwmarrin/discordgo" + "github.com/justinian/dice" + opentracing "github.com/opentracing/opentracing-go" +) + +func roll(ctx context.Context, s *discordgo.Session, m *discordgo.Message, parv []string) error { + sp, ctx := opentracing.StartSpanFromContext(ctx, "dice") + defer sp.Finish() + + if len(parv) != 2 { + return errors.New("not enough parameters (expected 1)") + } + + text, _, err := dice.Roll(parv[1]) + if err != nil { + return err + } + + _, err = s.ChannelMessageSend(m.ChannelID, text.String()) + return err +} diff --git a/cmd/vyvanse/gops.go b/cmd/vyvanse/gops.go new file mode 100644 index 0000000..184b656 --- /dev/null +++ b/cmd/vyvanse/gops.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/google/gops/agent" +) + +func init() { + if err := agent.Listen(nil); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/vyvanse/hipster.go b/cmd/vyvanse/hipster.go new file mode 100644 index 0000000..fcdeb00 --- /dev/null +++ b/cmd/vyvanse/hipster.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + "github.com/Xe/ln" + "github.com/bwmarrin/discordgo" + opentracing "github.com/opentracing/opentracing-go" +) + +func hipster(ctx context.Context, s *discordgo.Session, m *discordgo.Message, parv []string) error { + sp, ctx := opentracing.StartSpanFromContext(ctx, "hipster") + defer sp.Finish() + + msg, err := getHipsterText(ctx) + if err != nil { + ln.Error(ctx, err, ln.F{"action": "get hipster text"}) + return err + } + + _, err = s.ChannelMessageSend(m.ChannelID, msg) + return err +} + +func getHipsterText(ctx context.Context) (string, error) { + req, err := http.NewRequest(http.MethodGet, "http://hipsterjesus.com/api/?type=hipster-centric&html=false¶s=1", nil) + req = req.WithContext(ctx) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + + textStruct := &struct { + Text string `json:"text"` + }{} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + json.Unmarshal(body, textStruct) + + text := strings.Split(textStruct.Text, ". ")[0] + + return text, nil +} diff --git a/cmd/vyvanse/main.go b/cmd/vyvanse/main.go new file mode 100644 index 0000000..f56e06d --- /dev/null +++ b/cmd/vyvanse/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "git.xeserv.us/xena/vyvanse/bot" + "github.com/Xe/ln" + "github.com/bwmarrin/discordgo" + _ "github.com/joho/godotenv/autoload" + "github.com/namsral/flag" + xkcd "github.com/nishanths/go-xkcd" + opentracing "github.com/opentracing/opentracing-go" + otlog "github.com/opentracing/opentracing-go/log" + zipkin "github.com/openzipkin/zipkin-go-opentracing" + "github.com/robfig/cron" +) + +var lastXKCD int + +var ( + pesterChannel = flag.String("upload-channel", "", "Discord channel ID to upload and announce new XKCD items to") + token = flag.String("token", "", "discord bot token") + zipkinURL = flag.String("zipkin-url", "", "URL for Zipkin traces") +) + +func main() { + flag.Parse() + + xk := xkcd.NewClient() + dg, err := discordgo.New("Bot " + *token) + if err != nil { + log.Fatal(err) + } + + c := cron.New() + + comic, err := xk.Latest() + if err != nil { + log.Fatal(err) + } + lastXKCD = comic.Number + + c.AddFunc("@daily", func() { + sp, ctx := opentracing.StartSpanFromContext(context.Background(), "daily.xkcd.fetch") + + comic, err := xk.Latest() + if err != nil { + log.Println(err) + return + } + + if comic.Number > lastXKCD { + sp.LogFields(otlog.String("image.url", comic.ImageURL), otlog.String("target", *pesterChannel)) + + req, err := http.NewRequest(http.MethodGet, comic.ImageURL, nil) + if err != nil { + ln.Error(ctx, err, ln.F{"action": "make request"}) + sp.LogFields(otlog.Error(err)) + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + ln.Error(ctx, err, ln.F{"action": "http get", "url": comic.ImageURL}) + sp.LogFields(otlog.Error(err)) + return + } + + fname := fmt.Sprintf("%d - %s.png", comic.Number, comic.Title) + msg := fmt.Sprintf("New XKCD comic uploaded: %d - %s\n\n*%s*", comic.Number, comic.Title, comic.Alt) + _, err = dg.ChannelFileSendWithMessage(*pesterChannel, msg, fname, resp.Body) + if err != nil { + ln.Error(ctx, err, ln.F{"action": "send xkcd upload", "to": *pesterChannel}) + sp.LogFields(otlog.Error(err)) + return + } + + lastXKCD = comic.Number + } + }) + + c.Start() + + if *zipkinURL != "" { + collector, err := zipkin.NewHTTPCollector(*zipkinURL) + if err != nil { + ln.FatalErr(context.Background(), err) + } + tracer, err := zipkin.NewTracer( + zipkin.NewRecorder(collector, false, "vyvanse:5000", "vyvanse")) + if err != nil { + ln.FatalErr(context.Background(), err) + } + + opentracing.SetGlobalTracer(tracer) + } + + cs := bot.NewCommandSet() + cs.Prefix = ">" + + cs.AddCmd("hipster", "generates hipster-sounding text", bot.NoPermissions, hipster) + cs.AddCmd("printerfact", "facts about printers", bot.NoPermissions, printerFact) + cs.AddCmd("dice", "roll the dice", bot.NoPermissions, roll) + cs.AddCmd("splattus", "splatoon 2 map rotation status", bot.NoPermissions, spla2nMaps) + + dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { + err := cs.Run(s, m.Message) + if err != nil { + ln.Error(context.Background(), err, ln.F{"action": "run commandSet on message"}) + } + }) + + // Open the websocket and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + // Simple way to keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} diff --git a/cmd/vyvanse/printerfact.go b/cmd/vyvanse/printerfact.go new file mode 100644 index 0000000..f7c505b --- /dev/null +++ b/cmd/vyvanse/printerfact.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/bwmarrin/discordgo" + opentracing "github.com/opentracing/opentracing-go" +) + +func printerFact(ctx context.Context, s *discordgo.Session, m *discordgo.Message, parv []string) error { + sp, ctx := opentracing.StartSpanFromContext(ctx, "printer.fact") + defer sp.Finish() + + fact, err := getPrinterFact() + if err != nil { + return err + } + + s.ChannelMessageSend(m.ChannelID, fact) + return nil +} + +func getPrinterFact() (string, error) { + resp, err := http.Get("https://xena.stdlib.com/printerfacts") + if err != nil { + return "", err + } + + factStruct := &struct { + Facts []string `json:"facts"` + }{} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + json.Unmarshal(body, factStruct) + + text := fmt.Sprintf("%s", factStruct.Facts[0]) + + return text, nil +} diff --git a/cmd/vyvanse/spla2n.go b/cmd/vyvanse/spla2n.go new file mode 100644 index 0000000..e9981bd --- /dev/null +++ b/cmd/vyvanse/spla2n.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + opentracing "github.com/opentracing/opentracing-go" + otlog "github.com/opentracing/opentracing-go/log" +) + +func spla2nMaps(ctx context.Context, s *discordgo.Session, msg *discordgo.Message, parv []string) error { + sp, ctx := opentracing.StartSpanFromContext(ctx, "spla2nMaps") + defer sp.Finish() + + resp, err := http.Get("https://splatoon.ink/schedule2") + if err != nil { + sp.LogFields(otlog.Error(err), otlog.String("step", "http get")) + return err + } + + st := &splattus{} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + sp.LogFields(otlog.Error(err), otlog.String("step", "http response read")) + return err + } + + json.Unmarshal(body, st) + + var modeInfo []string + + for _, mode := range st.Modes.Regular { + if mode.Active() { + modeInfo = append(modeInfo, mode.String()) + } + } + + for _, mode := range st.Modes.Gachi { + if mode.Active() { + modeInfo = append(modeInfo, mode.String()) + } + } + + for _, mode := range st.Modes.League { + if mode.Active() { + modeInfo = append(modeInfo, mode.String()) + } + } + + text := strings.Join(modeInfo, "\n") + + _, err = s.ChannelMessageSend(msg.ChannelID, text) + return err +} + +type splatoonMode struct { + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + Maps []string `json:"maps"` + Rule splatoonRule `json:"rule"` + Mode splatoonGameMode `json:"mode"` +} + +func (sm splatoonMode) Active() bool { + beg := time.Unix(sm.StartTime, 0) + end := time.Unix(sm.EndTime, 0) + now := time.Now() + + return now.After(beg) && now.Before(end) +} + +func (sm splatoonMode) String() string { + maps := strings.Join(sm.Maps, ", ") + end := time.Unix(sm.EndTime, 0) + now := time.Now() + diff := end.Sub(now) + + return fmt.Sprintf("%s:\nRotation ends at %s (in %s)\nMaps: %s\nRule: %s\n", sm.Mode, end.Format(time.RFC3339), diff, maps, sm.Rule) +} + +type splatoonGameMode struct { + Key string `json:"key"` + Name string `json:"name"` +} + +func (sgm splatoonGameMode) String() string { + return sgm.Name +} + +type splatoonRule struct { + Key string `json:"key"` + MultilineName string `json:"multiline_name"` + Name string `json:"name"` +} + +func (sr splatoonRule) String() string { + return sr.Name +} + +type splattus struct { + UpdateTime int64 `json:"updateTime"` + Modes struct { + League []splatoonMode `json:"league"` + Regular []splatoonMode `json:"regular"` + Gachi []splatoonMode `json:"gachi"` + } `json:"modes"` +} diff --git a/docker-compose.yml b/docker-compose.yml index c14a478..316e33e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,9 @@ version: "3" services: + # Persistence layers and dev tools + + # http://127.0.0.1:9411 zipkin: image: openzipkin/zipkin environment: @@ -8,8 +11,26 @@ services: ports: - "9411:9411" + # message queue + mq: + image: drone/mq + + # database + rqlite: + restart: always + image: rqlite/rqlite:4.0.2 + volumes: + - rqlite:/rqlite/file + command: -on-disk -http-adv-addr rqlite:4001 + + # the bot and event sourcing ingress vyvanse: image: xena/vyvanse env_file: ./.env depends_on: - zipkin + - mq + - rqlite + +volumes: + rqlite: