Skip to content

Commit

Permalink
Merge pull request #35 from wneessen/feature/34_reputation-slashcommand
Browse files Browse the repository at this point in the history
Feature/34 reputation slashcommand
  • Loading branch information
wneessen authored Dec 4, 2022
2 parents 20602cb + 312bffe commit 4f524aa
Show file tree
Hide file tree
Showing 21 changed files with 434 additions and 32 deletions.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
14 changes: 13 additions & 1 deletion bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
APIURLSoTAllegiance = "https://www.seaofthieves.com/api/profilev2"
APIURLSoTSeasons = "https://www.seaofthieves.com/api/profilev2/seasons-progress"
APIURLSoTUserBalance = "https://www.seaofthieves.com/api/profilev2/balance"
APIURLSoTReputation = "https://www.seaofthieves.com/api/profilev2/reputation"
APIURLSoTUserOverview = "https://www.seaofthieves.com/api/profilev2/overview"
APIURLSoTEventHub = "https://www.seaofthieves.com/event-hub"
APIURLRTTradeRoutes = "https://maps.seaofthieves.rarethief.com/js/trade_routes.js"
Expand Down Expand Up @@ -141,6 +142,8 @@ func (b *Bot) Run() error {
defer rct.Stop()
ddt := time.NewTicker(b.Config.Timer.DDUpdate)
defer ddt.Stop()
urt := time.NewTicker(b.Config.Timer.URUpdate)
defer urt.Stop()

// Perform an update for all scheduled update tasks once if first-run flag is set
if b.Config.GetFirstRun() {
Expand All @@ -151,6 +154,9 @@ func (b *Bot) Run() error {
if err := b.ScheduledEventUpdateUserStats(); err != nil {
b.Log.Error().Msgf("failed to update user stats: %s", err)
}
if err := b.ScheduledEventUpdateUserReputation(); err != nil {
b.Log.Error().Msgf("failed to update user reputation: %s", err)
}
if err := b.ScheduledEventUpdateDailyDeeds(); err != nil {
b.Log.Error().Msgf("failed to update daily deeds: %s", err)
}
Expand Down Expand Up @@ -200,7 +206,13 @@ func (b *Bot) Run() error {
case <-ust.C:
go func() {
if err := b.ScheduledEventUpdateUserStats(); err != nil {
ll.Error().Msgf("failed to process scheuled traderoute update event: %s", err)
ll.Error().Msgf("failed to process scheuled user stats update event: %s", err)
}
}()
case <-urt.C:
go func() {
if err := b.ScheduledEventUpdateUserReputation(); err != nil {
ll.Error().Msgf("failed to process scheuled user reputation update event: %s", err)
}
}()
case <-rct.C:
Expand Down
32 changes: 32 additions & 0 deletions bot/format.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package bot

import "strings"

// List of icons/emojis
const (
IconGold = "\U0001F7E1"
Expand All @@ -26,3 +28,33 @@ func changeIcon[V int | int64 | float32 | float64](v V) string {
}
return IconDecrease
}

// dbEmissaryToName converts the emissary name in the DB to the human readable format
func dbEmissaryToName(e string) string {
// factiong|hunterscall|merchantalliance|bilgerats|talltales|athenasfortune|` +
// `goldhoarders|orderofsouls|reapersbones
switch strings.ToLower(e) {
case "factiong":
return "Guardians of Fortune"
case "factionb":
return "Servants of the Flame"
case "hunterscall":
return "Hunter's Call"
case "merchantalliance":
return "Merchant Alliance"
case "bilgerats":
return "Bilge Rats"
case "talltales":
return "Tall Tales"
case "athenasfortune":
return "Athena's Forutne"
case "goldhoarders":
return "Gold Hoarders"
case "orderofsouls":
return "Order of Souls"
case "reapersbones":
return "Reaper's Bones"
default:
return ""
}
}
5 changes: 4 additions & 1 deletion bot/sc_handler_sot_allegiance.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type SoTAllegiance struct {
ShipsSunk int64
MaxStreak int64
TotalGold int64
Icon string
}

// SlashCmdSoTAllegiance handles the /allegiance slash command
Expand Down Expand Up @@ -81,7 +82,7 @@ func (b *Bot) SlashCmdSoTAllegiance(s *discordgo.Session, i *discordgo.Interacti
{
Title: fmt.Sprintf("Your current allegiance values for the **%s**:", a.Allegiance),
Thumbnail: &discordgo.MessageEmbedThumbnail{
URL: fmt.Sprintf("%s/allegiance/%s.png", AssetsBaseURL, al),
URL: fmt.Sprintf("%s/factions/%s.png", AssetsBaseURL, a.Icon),
},
Type: discordgo.EmbedTypeRich,
Fields: ef,
Expand Down Expand Up @@ -148,6 +149,7 @@ func (b *Bot) SoTGetAllegiance(rq *Requester, at string) (SoTAllegiance, error)
a.TotalGold = v
}
a.Allegiance = "Guardians of Fortune"
a.Icon = "factiong"
}
case "servants":
for _, d := range al.Stats {
Expand All @@ -168,6 +170,7 @@ func (b *Bot) SoTGetAllegiance(rq *Requester, at string) (SoTAllegiance, error)
}
}
a.Allegiance = "Servants of the Flame"
a.Icon = "factionb"
}

return a, nil
Expand Down
211 changes: 211 additions & 0 deletions bot/sc_handler_sot_reputation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package bot

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"time"

"github.com/wneessen/arrgo/crypto"
"github.com/wneessen/arrgo/model"

"github.com/bwmarrin/discordgo"
)

// SoTReputation represents first level of the JSON structure of the Sea of Thieves reputation
// within a season API response
type SoTReputation map[string]SoTFactionReputation

// SoTFactionReputation represents second level of the JSON structure of the Sea of Thieves reputation
// within a season API response
type SoTFactionReputation struct {
Name string
Motto string `json:"Motto"`
Rank string `json:"Rank"`
Level int64 `json:"Level"`
Experience int64 `json:"XP"`
NextCompanyLevel SoTFactionNextLevel `json:"NextCompanyLevel"`
TitlesTotal int64 `json:"TitlesTotal"`
TitlesUnlocked int64 `json:"TitlesUnlocked"`
EmblemsTotal int64 `json:"EmblemsTotal"`
EmblemsUnlocked int64 `json:"EmblemsUnlocked"`
ItemsTotal int64 `json:"ItemsTotal"`
ItemsUnlocked int64 `json:"ItemsUnlocked"`
}

// SoTFactionNextLevel represents XP level information of the JSON structure of the Sea of Thieves reputation
// within a season API response
type SoTFactionNextLevel struct {
Level int64 `json:"Level"`
XPRequired int64 `json:"XpRequiredToAttain"`
}

// SlashCmdSoTReputation handles the /reputation slash command
func (b *Bot) SlashCmdSoTReputation(s *discordgo.Session, i *discordgo.InteractionCreate) error {
fo := i.ApplicationCommandData().Options
if len(fo) <= 0 {
return fmt.Errorf("no option given")
}
rc, ok := fo[0].Value.(string)
if !ok {
return fmt.Errorf("provided option value is not a string")
}

re, err := regexp.Compile(`^(?i:factiong|hunterscall|merchantalliance|bilgerats|athenasfortune|` +
`goldhoarders|orderofsouls|reapersbones|factionb)$`)
if err != nil {
return err
}
faa := re.FindStringSubmatch(rc)
if len(faa) != 1 {
return fmt.Errorf("failed to parse value string")
}
fa := faa[0]
_ = fa

r, err := b.NewRequester(i.Interaction)
if err != nil {
return err
}

if err := b.StoreSoTUserReputation(r.User); err != nil {
b.Log.Warn().Msgf("failed to store user reputation data to database")
}
ur, err := b.Model.UserReputation.GetByUserID(r.User.ID, fa)
if err != nil {
return err
}

var ef []*discordgo.MessageEmbedField
ef = append(ef, &discordgo.MessageEmbedField{
Name: "Motto",
Value: ur.Motto,
Inline: false,
})
if ur.Rank != "" {
ef = append(ef, &discordgo.MessageEmbedField{
Name: "Rank",
Value: ur.Rank,
Inline: false,
})
}
ef = append(ef, &discordgo.MessageEmbedField{
Name: "Level",
Value: fmt.Sprintf("%s **%d**", IconGauge, ur.Level),
Inline: true,
})
ef = append(ef, &discordgo.MessageEmbedField{
Name: "XP in current level",
Value: fmt.Sprintf("%s **%d/%d**", IconIncrease, ur.Experience, ur.ExperienceNextLevel),
Inline: true,
})

e := []*discordgo.MessageEmbed{
{
Title: fmt.Sprintf("Your user reputation with **%s**", dbEmissaryToName(ur.Emissary)),
Thumbnail: &discordgo.MessageEmbedThumbnail{
URL: fmt.Sprintf("%s/factions/%s.png", AssetsBaseURL, fa),
},
Type: discordgo.EmbedTypeRich,
Fields: ef,
},
}
if _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Embeds: &e}); err != nil {
return err
}

return nil
}

// SoTGetReputation returns the parsed API response from the Sea of Thieves reputation API
func (b *Bot) SoTGetReputation(rq *Requester) (SoTReputation, error) {
var re SoTReputation
hc, err := NewHTTPClient()
if err != nil {
return re, fmt.Errorf(ErrFailedHTTPClient, err)
}
c, err := rq.GetSoTRATCookie()
if err != nil {
return re, err
}
r, err := hc.HTTPReq(APIURLSoTReputation, ReqMethodGet, nil)
if err != nil {
return re, err
}
r.SetSOTRequest(c)
rd, ho, err := hc.Fetch(r)
if err != nil {
return re, err
}
if ho.StatusCode == http.StatusUnauthorized {
return re, ErrSOTUnauth
}
if err := json.Unmarshal(rd, &re); err != nil {
return re, err
}
return re, nil
}

// StoreSoTUserReputation will retrieve the latest user reputation from the API and store them in the DB
func (b *Bot) StoreSoTUserReputation(u *model.User) error {
r, err := NewRequesterFromUser(u, b.Model.User)
if err != nil {
b.Log.Warn().Msgf("failed to create new requester: %s", err)
return err
}
ur, err := b.SoTGetReputation(r)
if err != nil {
switch {
case errors.Is(err, ErrSOTUnauth):
b.Log.Warn().Msgf("failed to fetch user reputation - RAT token is expired")
return nil
default:
return fmt.Errorf("failed to fetch user reputation for user %s: %w", u.UserID, err)
}
}
for k, rep := range ur {
dur := &model.UserReputation{
UserID: u.ID,
Emissary: k,
Motto: rep.Motto,
Rank: rep.Rank,
Level: rep.Level,
Experience: rep.Experience,
NextLevel: rep.NextCompanyLevel.Level,
ExperienceNextLevel: rep.NextCompanyLevel.XPRequired,
TitlesTotal: rep.TitlesTotal,
TitlesUnlocked: rep.TitlesUnlocked,
EmblemsTotal: rep.EmblemsTotal,
EmblemsUnlocked: rep.EmblemsUnlocked,
ItemsTotal: rep.ItemsTotal,
ItemsUnlocked: rep.ItemsUnlocked,
}
if err := b.Model.UserReputation.Insert(dur); err != nil {
return fmt.Errorf("failed to store user reputation for user %q in DB: %w", u.UserID, err)
}
}
return nil
}

// ScheduledEventUpdateUserReputation performs scheuled updates of the SoT user reputation for each user
func (b *Bot) ScheduledEventUpdateUserReputation() error {
ll := b.Log.With().Str("context", "bot.ScheduledEventUpdateUserReputation").Logger()
ul, err := b.Model.User.GetUsers()
if err != nil {
return fmt.Errorf("failed to retrieve user list from DB: %w", err)
}
for _, u := range ul {
if err := b.StoreSoTUserReputation(u); err != nil {
ll.Error().Msgf("failed to store user reputation in DB: %s", err)
continue
}
rd, err := crypto.RandDuration(10, "s")
if err != nil {
rd = time.Second * 10
}
time.Sleep(rd)
}
return nil
}
40 changes: 21 additions & 19 deletions bot/slashcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,28 +182,29 @@ func (b *Bot) getSlashCommands() []*discordgo.ApplicationCommand {
},
},

/*
// reputation provides the current emissary reputation value in the different factions
{
Name: "reputation",
Description: "Returns your current reputation value in the different emissary factions",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "emissary-faction",
Description: "Name of the emissary faction",
Required: true,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{Name: "Athena's Fortune", Value: "athena"},
{Name: "Gold Hoarder", Value: "hoarder"},
{Name: "Merchant Alliance", Value: "merchant"},
{Name: "Order of Souls", Value: "order"},
{Name: "Reaper's Bone", Value: "reaper"},
},
// reputation provides the current emissary reputation value in the different factions
{
Name: "reputation",
Description: "Returns your current reputation value in the different emissary/allegiance faction",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "emissary-or-allegiance-faction",
Description: "Name of the emissary/allegiance faction",
Required: true,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{Name: "Athena's Fortune", Value: "athenasfortune"},
{Name: "Gold Hoarder", Value: "goldhoarders"},
{Name: "Merchant Alliance", Value: "merchantalliance"},
{Name: "Order of Souls", Value: "orderofsouls"},
{Name: "Reaper's Bone", Value: "reapersbones"},
{Name: "Hunter's Call", Value: "hunterscall"},
{Name: "Servants of the Flame", Value: "factionb"},
{Name: "Guardians of Fortune", Value: "factiong"},
},
},
},
*/
},

// allegiance provides the current allegiance values in the different factions
{
Expand Down Expand Up @@ -369,6 +370,7 @@ func (b *Bot) SlashCommandHandler(s *discordgo.Session, i *discordgo.Interaction
"dailydeeds": b.SlashCmdSoTDailyDeeds,
"ledger": b.SlashCmdSoTLedger,
"allegiance": b.SlashCmdSoTAllegiance,
"reputation": b.SlashCmdSoTReputation,
}

// Define list of slash commands that should use ephemeral messages
Expand Down
2 changes: 1 addition & 1 deletion bot/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package bot

const Version = "0.2.7"
const Version = "0.2.8"
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Config struct {
FHSpam int `fig:"flameheart_spam" default:"60"`
TRUpdate time.Duration `fig:"traderoutes_update" default:"12h"`
USUpdate time.Duration `fig:"userstats_update" default:"6h"`
URUpdate time.Duration `fig:"userrep_update" default:"24h"`
RCCheck time.Duration `fig:"ratcookie_check" default:"6h"`
DDUpdate time.Duration `fig:"dailydeed_update" default:"24h"`
ULUpdate time.Duration `fig:"userledger_update" default:"6h"`
Expand Down
Loading

0 comments on commit 4f524aa

Please sign in to comment.