commit 82e5b12a48c5af20503943aa43cfdf3c26dcdfdc from: Witcher01 date: Sun Mar 7 22:50:58 2021 UTC initial commit commit - /dev/null commit + 82e5b12a48c5af20503943aa43cfdf3c26dcdfdc blob - /dev/null blob + 3a46bd7d55f89caf5b0178268117e8856e6b9dc7 (mode 644) --- /dev/null +++ go.mod @@ -0,0 +1,9 @@ +module github.com/Witcher01/discord_sr + +go 1.13 + +require ( + github.com/Witcher01/srapi v0.0.0-20210305194725-c19b54f7ba71 + github.com/akamensky/argparse v1.2.2 + github.com/bwmarrin/discordgo v0.23.2 +) blob - /dev/null blob + 235c3c79462c988b480c9f1e6b330505c957c02b (mode 644) --- /dev/null +++ go.sum @@ -0,0 +1,10 @@ +github.com/Witcher01/srapi v0.0.0-20210305194725-c19b54f7ba71 h1:mP7nJhpQpSYKgVlzhfb1kcO1H9LZzzRWC6Fczu8RgSM= +github.com/Witcher01/srapi v0.0.0-20210305194725-c19b54f7ba71/go.mod h1:zkV6ShyNJYwzWIrB+MlXnPtDB81XlaYh208VeUw+lHo= +github.com/akamensky/argparse v1.2.2 h1:P17T0ZjlUNJuWTPPJ2A5dM1wxarHgHqfYH+AZTo2xQA= +github.com/akamensky/argparse v1.2.2/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= +github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= +github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= blob - /dev/null blob + 35ed6b8e761e680d734a398d5da016c751d766bc (mode 644) --- /dev/null +++ main.go @@ -0,0 +1,147 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/signal" + "regexp" + "strings" + "syscall" + + "github.com/Witcher01/discord_sr/srcom" + "github.com/bwmarrin/discordgo" + + "github.com/akamensky/argparse" +) + +type commandArgs struct { + game_id *string + category *string + amount *int + platform *string +} + +func main() { + parser := argparse.NewParser("discord_sr", "A bot for discord that returns simple queries for speedrun.com") + + token := parser.String("t", "token", &argparse.Options{Required: true, Help: "Authentication token to use for the bot"}) + err := parser.Parse(os.Args) + if err != nil { + fmt.Println(parser.Usage(err)) + return + } + + discord, err := discordgo.New("Bot " + *token) + + // Cleanly close down the Discord session on program end + defer discord.Close() + + if err != nil { + fmt.Println("error creating bot,", err) + return + } + + discord.AddHandler(messageCreate); + + discord.Identify.Intents = discordgo.IntentsGuildMessages + + err = discord.Open() + + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + // Wait here until CTRL-C or other term signal is received + fmt.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc +} + +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author.ID == s.State.User.ID { + return + } + + if m.Content == "*ping" { + s.ChannelMessageSend(m.ChannelID, "Pong!") + return + } + + if m.Content == "*pong" { + s.ChannelMessageSend(m.ChannelID, "Ping!") + return + } + + if strings.HasPrefix(m.Content, "*records") { + args, err := leaderboardArgsParser(m.Content, "records", "Return the name of the top runners for a specified game") + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + return + } + + top, err := srcom.GetTopRunnersByGame(*args.game_id, *args.category, *args.amount, *args.platform) + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + return + } + + s.ChannelMessageSend(m.ChannelID, top) + return + } + + if strings.HasPrefix(m.Content, "*top") { + args, err := leaderboardArgsParser(m.Content, "top", "Return the top runs of a game") + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + return + } + + top, err := srcom.GetTopByGame(*args.game_id, *args.category, *args.amount, *args.platform) + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + return + } + + s.ChannelMessageSend(m.ChannelID, top) + return + } + + if strings.HasPrefix(m.Content, "*wr") { + args, err := leaderboardArgsParser(m.Content, "wr", "Return the wr of the specified game") + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + return + } + + wr, err := srcom.GetWRByGame(*args.game_id, *args.category, *args.platform) + if err != nil { + s.ChannelMessageSend(m.ChannelID, err.Error()) + } + + s.ChannelMessageSend(m.ChannelID, wr) + return + } +} + +func leaderboardArgsParser(input string, name string, desc string) (*commandArgs, error) { + parser := argparse.NewParser(name, desc) + gameid := parser.String("i", "game-id", &argparse.Options{Required: true, Help: "The game id found in the URL of the leaderboard"}) + amount := parser.Int("a", "amount", &argparse.Options{Required: false, Help: "The amount of records to be returned", Default: 3}) + platform := parser.String("p", "platform", &argparse.Options{Required: false, Help: "The platform for which the leaderboard should be returned", Default: *new(string)}) + category := parser.String("c", "category", &argparse.Options{Required: false, Help: "The category for which the leaderboard should be returned", Default: *new(string)}) + + r := regexp.MustCompile(`[^\s"']+|"([^"]*)"|'([^']*)`) + split := r.FindAllString(input, -1) + err := parser.Parse(split) + if err != nil { + return nil, errors.New(parser.Usage(err)) + } + + // trim quotation marks from category to allow category names with spaces + *category = strings.Trim(*category, "\"") + + return &commandArgs{gameid, category, amount, platform}, nil +} blob - /dev/null blob + 71869a2d547c2ffcdac31e405290a2f34a5d4ffa (mode 644) --- /dev/null +++ srcom/srcom.go @@ -0,0 +1,169 @@ +package srcom + +import ( + "errors" + "strings" + + "github.com/Witcher01/srapi" +) + +func GetTopRunnersByGame(id string, category string, amount int, platform string) (string, error) { + collection, err := getLeaderboard(id, category, amount, platform) + if err != nil { + return "", err + } + + var players_sb strings.Builder + for _, run := range collection.Runs { + players, err := run.Run.Players() + if err != nil { + return "", err + } + + for _, player := range players.Players() { + players_sb.WriteString(player.Name()) + players_sb.WriteString(", ") + } + } + ret := players_sb.String() + ret = strings.TrimSuffix(ret, ", ") + + return ret, nil +} + +func GetTopByGame(id string, category string, amount int, platform string) (string, error) { + collection, err := getLeaderboard(id, category, amount, platform) + if err != nil { + return "", err + } + + var pbs strings.Builder + for _, run := range collection.Runs { + time, _err := getFormattedPlayerTime(run) + if err != nil { + return "", _err + } + + pbs.WriteString(time) + pbs.WriteRune('\n') + } + + return pbs.String(), nil +} + +func GetWRByGame(id string, category string, platform string) (string, error) { + collection, err := getLeaderboard(id, category, 1, platform) + if err != nil { + return "", err + } + + wr := collection.Runs[0] + + time, _err := getFormattedPlayerTime(wr) + if _err != nil { + return "", err + } + + return time, nil +} + +func getFormattedTime(run srapi.RankedRun) string { + times := run.Run.Times + var time string + if times.IngameTime.String() != "0s" { + time = run.Run.Times.IngameTime.Format() + " (IGT)" + } else if times.RealtimeWithoutLoads.String() != "0s" { + time = run.Run.Times.RealtimeWithoutLoads.Format() + " (RTA/NL)" + } else { + time = run.Run.Times.Realtime.Format() + " (RTA)" + } + + return time +} + +func getFormattedPlayerTime(run srapi.RankedRun) (string, error) { + players, err := run.Run.Players() + if err != nil { + return "", err + } + + var sb strings.Builder + for _, player := range players.Players() { + sb.WriteString(player.Name()) + sb.WriteString(", "); + } + sb_string := sb.String() + sb_string = strings.TrimSuffix(sb_string, ", ") + + time := getFormattedTime(run) + + sb.Reset() + sb.WriteString(sb_string) + sb.WriteString(": ") + sb.WriteString(time) + + return sb.String(), nil +} + +func getDefaultGameLeaderboard(id string, amount int, platform string) (*srapi.Leaderboard, error) { + game, err := srapi.GameByID(id, srapi.NoEmbeds) + if err != nil { + return nil, err + } + + filter := &srapi.LeaderboardOptions{Top: amount, Platform: platform} + + collection, err := game.PrimaryLeaderboard(filter, srapi.NoEmbeds) + if err != nil { + return nil, err + } + + return collection, nil +} + +func getGameLeaderboardByCategory(id string, category string, amount int, platform string) (*srapi.Leaderboard, error) { + embeds := srapi.NoEmbeds + if category != "" { + embeds = category + } + + game, err := srapi.GameByID(id, embeds) + if err != nil { + return nil, err + } + + cats, err := game.Categories(nil, nil, srapi.NoEmbeds) + if err != nil { + return nil, err + } + + var cat *srapi.Category = nil + cats.Walk(func(c *srapi.Category) bool { + if (strings.ToLower(c.Name) == strings.ToLower(category)) { + cat = c + return false + } + return true + }); + + if cat == nil { + return nil, errors.New("No category with that name found.") + } + + filter_options := &srapi.LeaderboardOptions{Top: amount, Platform: platform} + + collection, err := cat.PrimaryLeaderboard(filter_options, srapi.NoEmbeds) + if err != nil { + return nil, err + } + + return collection, nil +} + +func getLeaderboard(id string, category string, amount int, platform string) (*srapi.Leaderboard, error) { + if category != "" { + return getGameLeaderboardByCategory(id, category, amount, platform) + } + + return getDefaultGameLeaderboard(id, amount, platform) +}