package main import ( "context" "fmt" "log" "net/http" "strings" "git.xeserv.us/xena/gorqlite" "git.xeserv.us/xena/vyvanse/bot" "git.xeserv.us/xena/vyvanse/internal/dao" "github.com/Xe/ln" "github.com/bwmarrin/discordgo" "github.com/drone/mq/stomp" _ "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") databaseURL = flag.String("database-url", "http://", "URL for database (rqlite)") mqURL = flag.String("mq-url", "tcp://mq:9000", "URL for STOMP server") ) func main() { flag.Parse() 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) } xk := xkcd.NewClient() dg, err := discordgo.New("Bot " + *token) if err != nil { log.Fatal(err) } db, err := gorqlite.Open(*databaseURL) if err != nil { log.Fatal(err) } us := dao.NewUsers(db) type migrator interface { Migrate(ctx context.Context) error } mgs := []migrator{us} sp, ctx := opentracing.StartSpanFromContext(context.Background(), "migrations") for _, mg := range mgs { err := mg.Migrate(ctx) if err != nil { sp.Finish() ln.FatalErr(ctx, err) } } sp.Finish() ctx = context.Background() mq, err := stomp.Dial(*mqURL) if err != nil { ln.FatalErr(ctx, err, ln.F{"url": *mqURL}) } _ = mq 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() 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) cs.AddCmd("top10", "shows the top 10 chatters on this server", bot.NoPermissions, top10(us)) dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { sp, ctx := opentracing.StartSpanFromContext(context.Background(), "message.create.post.stomp") defer sp.Finish() f := ln.F{ "channel_id": m.ChannelID, "message_id": m.ID, "message_author": m.Author.ID, "message_author_name": m.Author.Username, "message_author_is_bot": m.Author.Bot, } err := mq.SendJSON("/topic/message_create", m.Message) if err != nil { if err.Error() == "EOF" { mq, err = stomp.Dial(*mqURL) if err != nil { ln.Error(ctx, err, f, ln.F{"url": *mqURL, "action": "reconnect to mq"}) return } err = mq.SendJSON("/topic/message_create", m.Message) if err != nil { ln.Error(ctx, err, f, ln.F{"action": "retry message_create post to message queue"}) return } return } ln.Error(ctx, err, f, ln.F{"action": "send created message to queue"}) return } ln.Log(ctx, f, ln.F{"action": "message_create"}) }) dg.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberAdd) { sp, ctx := opentracing.StartSpanFromContext(context.Background(), "member.add.post.stomp") defer sp.Finish() f := ln.F{ "guild_id": m.GuildID, "user_id": m.User.ID, "user_name": m.User.Username, } err := mq.SendJSON("/topic/member_add", m.Member) if err != nil { ln.Error(ctx, err, f, ln.F{"action": "send added member to queue"}) } ln.Log(ctx, f, ln.F{"action": "member_add"}) }) dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.ID == s.State.User.ID { return } sp, ctx := opentracing.StartSpanFromContext(context.Background(), "discordgo.message.create") defer sp.Finish() err := cs.Run(ctx, s, m.Message) if err != nil { ln.Error(ctx, err, ln.F{"action": "run commandSet on message"}) } _, err = us.Insert(ctx, m.Author.ID) if err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") { ln.Error(ctx, err, ln.F{"action": "insert user into database"}) } err = us.IncScore(ctx, m.Author.ID) if err != nil { ln.Error(ctx, err, ln.F{"action": "increment user score"}) } }) // Open the websocket and begin listening. err = dg.Open() if err != nil { fmt.Println("error opening connection,", err) return } ln.Log(ctx, ln.F{"action": "bot is running"}) for { select {} } }