831 lines
22 KiB
Go
831 lines
22 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"entgo.io/ent/dialect/sql"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/an0nfunc/go-steamapi"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/time/rate"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"somegit.dev/csgowtf/csgowtfd/ent"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/match"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/matchplayer"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/player"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/roundstats"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/spray"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/weapon"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Conf struct {
|
|
Logging struct {
|
|
Level string
|
|
}
|
|
DB struct {
|
|
Driver string
|
|
ConnectTo string `yaml:"connect_to"`
|
|
} `yaml:"db"`
|
|
Parser struct {
|
|
Worker int
|
|
}
|
|
Steam struct {
|
|
Username string
|
|
Password string
|
|
AuthCode string `yaml:"auth_code"`
|
|
APIKey string `yaml:"api_key"`
|
|
RatePerSecond float64 `yaml:"rate_per_sec"`
|
|
Sentry string
|
|
LoginKey string `yaml:"login_key"`
|
|
MaxRetryWait int `yaml:"max_retry_wait"`
|
|
}
|
|
Redis struct {
|
|
Address string
|
|
Password string
|
|
}
|
|
Httpd struct {
|
|
CORSAllowDomains []string `yaml:"cors_allow_domains"`
|
|
Listen []struct {
|
|
Socket string
|
|
Host string
|
|
Port int
|
|
}
|
|
}
|
|
Csgowtfd struct {
|
|
ProfileUpdate string `yaml:"profile_update"`
|
|
SharecodeUpdate string `yaml:"sharecode_update"`
|
|
DemosExpire int `yaml:"demos_expire"`
|
|
SprayTimeout int `yaml:"spray_timeout"`
|
|
Timeout struct {
|
|
Read int
|
|
Write int
|
|
Idle int
|
|
}
|
|
}
|
|
DeepL struct {
|
|
BaseURL string `yaml:"base_url"`
|
|
APIKey string `yaml:"api_key"`
|
|
Timeout int `yaml:"timeout"`
|
|
} `yaml:"deepl"`
|
|
}
|
|
|
|
type CommunityXML struct {
|
|
SteamID64 uint64 `xml:"steamID64"`
|
|
AvatarURL string `xml:"avatarFull"`
|
|
VacBanned bool `xml:"vacBanned"`
|
|
ProfileName string `xml:"steamID"`
|
|
Error string `xml:"error"`
|
|
VanityURL string `xml:"customURL"`
|
|
}
|
|
|
|
type ShareCodeResponse struct {
|
|
Result struct {
|
|
Code string `json:"nextcode"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type DeepLResponse struct {
|
|
Translations []struct {
|
|
DetectedSourceLanguage string `json:"detected_source_language"`
|
|
Text string `json:"text"`
|
|
} `json:"translations"`
|
|
}
|
|
|
|
type MatchStats struct {
|
|
Win int `json:"win,omitempty"`
|
|
Tie int `json:"tie,omitempty"`
|
|
Loss int `json:"loss,omitempty"`
|
|
}
|
|
|
|
type MultiKills struct {
|
|
Duo uint `json:"duo,omitempty"`
|
|
Triple uint `json:"triple,omitempty"`
|
|
Quad uint `json:"quad,omitempty"`
|
|
Pent uint `json:"pent,omitempty"`
|
|
}
|
|
|
|
type Rank struct {
|
|
Old int `json:"old,omitempty"`
|
|
New int `json:"new,omitempty"`
|
|
}
|
|
|
|
type Damage struct {
|
|
Enemy uint `json:"enemy,omitempty"`
|
|
Team uint `json:"team,omitempty"`
|
|
}
|
|
|
|
type SelfTeamEnemy struct {
|
|
Self interface{} `json:"self,omitempty"`
|
|
Team interface{} `json:"team,omitempty"`
|
|
Enemy interface{} `json:"enemy,omitempty"`
|
|
}
|
|
|
|
type Flash struct {
|
|
Duration *SelfTeamEnemy `json:"duration,omitempty"`
|
|
Total *SelfTeamEnemy `json:"total,omitempty"`
|
|
}
|
|
|
|
type StatsResponse struct {
|
|
TeamID int `json:"team_id"`
|
|
Kills int `json:"kills"`
|
|
Deaths int `json:"deaths"`
|
|
Assists int `json:"assists"`
|
|
Headshot int `json:"headshot"`
|
|
MVP uint `json:"mvp"`
|
|
Score int `json:"score"`
|
|
Player interface{} `json:"player,omitempty"`
|
|
Rank *Rank `json:"rank,omitempty"`
|
|
MultiKills *MultiKills `json:"multi_kills,omitempty"`
|
|
Dmg *Damage `json:"dmg,omitempty"`
|
|
Flash *Flash `json:"flash,omitempty"`
|
|
Crosshair string `json:"crosshair,omitempty"`
|
|
Color string `json:"color,omitempty"`
|
|
KAST int `json:"kast,omitempty"`
|
|
}
|
|
|
|
type PlayerResponse struct {
|
|
SteamID64 uint64 `json:"steamid64,string"`
|
|
Name string `json:"name,omitempty"`
|
|
Avatar string `json:"avatar,omitempty"`
|
|
VAC bool `json:"vac"`
|
|
VACDate int64 `json:"vac_date,omitempty"`
|
|
GameBan bool `json:"game_ban"`
|
|
GameBanDate int64 `json:"game_ban_date,omitempty"`
|
|
Tracked bool `json:"tracked"`
|
|
VanityURL string `json:"vanity_url,omitempty"`
|
|
MatchStats *MatchStats `json:"match_stats,omitempty"`
|
|
Matches []*MatchResponse `json:"matches,omitempty"`
|
|
}
|
|
|
|
type MateResponse struct {
|
|
Player *PlayerResponse `json:"player"`
|
|
WinRate float32 `json:"win_rate,omitempty"`
|
|
TieRate float32 `json:"tie_rate,omitempty"`
|
|
Total int `json:"total,omitempty"`
|
|
}
|
|
|
|
type ChatResponse struct {
|
|
Player *PlayerResponse `json:"player,omitempty"`
|
|
Message string `json:"message"`
|
|
AllChat bool `json:"all_chat"`
|
|
Tick int `json:"tick"`
|
|
TranslatedFrom string `json:"translated_from,omitempty"`
|
|
TranslatedTo string `json:"translated_to,omitempty"`
|
|
}
|
|
|
|
type WeaponDmg struct {
|
|
Eq int `json:"eq"`
|
|
Dmg uint `json:"dmg"`
|
|
}
|
|
|
|
type MetaStatsResponse struct {
|
|
Player *PlayerResponse `json:"player"`
|
|
BestMates []*MateResponse `json:"best_mates,omitempty"`
|
|
MostMates []*MateResponse `json:"most_mates,omitempty"`
|
|
EqMap map[int]string `json:"eq_map,omitempty"`
|
|
WeaponDmg []*WeaponDmg `json:"weapon_dmg,omitempty"`
|
|
WinMaps map[string]float32 `json:"win_maps,omitempty"`
|
|
TieMaps map[string]float32 `json:"tie_maps,omitempty"`
|
|
TotalMaps map[string]int `json:"total_maps,omitempty"`
|
|
}
|
|
|
|
type MatchResponse struct {
|
|
MatchID uint64 `json:"match_id,string"`
|
|
ShareCode string `json:"share_code,omitempty"`
|
|
Map string `json:"map"`
|
|
Date int64 `json:"date"`
|
|
Score [2]int `json:"score"`
|
|
Duration int `json:"duration"`
|
|
MatchResult int `json:"match_result"`
|
|
MaxRounds int `json:"max_rounds,omitempty"`
|
|
Parsed bool `json:"parsed"`
|
|
ReplayURL string `json:"replay_url,omitempty"`
|
|
VAC bool `json:"vac"`
|
|
GameBan bool `json:"game_ban"`
|
|
Stats interface{} `json:"stats,omitempty"`
|
|
AvgRank float64 `json:"avg_rank,omitempty"`
|
|
TickRate float64 `json:"tick_rate,omitempty"`
|
|
}
|
|
|
|
var (
|
|
ErrorSharecodeNoMatch = errors.New("sharecode not provided")
|
|
ErrorAuthcodeRateLimit = errors.New("temporary rate-limited")
|
|
ErrorAuthcodeUnavailable = errors.New("temporary unavailable")
|
|
ErrorAuthcodeUnauthorized = errors.New("authcode unauthorized")
|
|
ErrorNoMatch = errors.New("no match found")
|
|
)
|
|
|
|
const (
|
|
shareCodeURLEntry = "https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=%s&steamid=%d&steamidkey=%s&knowncode=%s"
|
|
CachePrefix = "csgowtfd_"
|
|
SideMetaCacheKey = CachePrefix + "side_meta_%d"
|
|
MatchChatCacheKey = CachePrefix + "chat_%d_%s"
|
|
)
|
|
|
|
//goland:noinspection SpellCheckingInspection
|
|
var (
|
|
SteamID64RegEx = regexp.MustCompile(`^\d{17}$`)
|
|
ShareCodeRegEx = regexp.MustCompile(`^CSGO(?:-?[ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789]{5}){5}$`)
|
|
AuthCodeRegEx = regexp.MustCompile(
|
|
`^[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{5}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}$`)
|
|
)
|
|
|
|
func GetMetaStats(dbPlayer *ent.Player) (*MetaStatsResponse, error) {
|
|
mResponse := new(MetaStatsResponse)
|
|
mResponse.Player = &PlayerResponse{SteamID64: dbPlayer.ID}
|
|
|
|
tPlayers, err := dbPlayer.QueryMatches().QueryPlayers().Select(player.FieldID).All(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
matchIDs, err := dbPlayer.QueryMatches().IDs(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mapWins := map[string]int{}
|
|
mapTies := map[string]int{}
|
|
mapMatchTotal := map[string]int{}
|
|
matchSeen := map[uint64]bool{}
|
|
mResponse.EqMap = map[int]string{}
|
|
|
|
for _, s := range tPlayers {
|
|
if s.ID == dbPlayer.ID {
|
|
continue
|
|
}
|
|
|
|
mateRes := new(MateResponse)
|
|
mostRes := new(MateResponse)
|
|
|
|
playerRes := &PlayerResponse{
|
|
SteamID64: s.ID,
|
|
}
|
|
|
|
pMatches, err := s.QueryMatches().
|
|
Select(match.FieldID, match.FieldMatchResult, match.FieldMap).
|
|
Where(match.IDIn(matchIDs...)).
|
|
WithStats(). // TODO: limit fields returned
|
|
Where(match.HasStatsWith(matchplayer.Or(matchplayer.PlayerStats(dbPlayer.ID), matchplayer.PlayerStats(s.ID)))).
|
|
All(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mostRes.Player = playerRes
|
|
var wins int
|
|
var ties int
|
|
|
|
for _, pm := range pMatches {
|
|
var subjectStats *ent.MatchPlayer
|
|
var currentStats *ent.MatchPlayer
|
|
|
|
for _, ps := range pm.Edges.Stats {
|
|
if ps.PlayerStats == dbPlayer.ID {
|
|
subjectStats = ps
|
|
} else if ps.PlayerStats == s.ID {
|
|
currentStats = ps
|
|
}
|
|
}
|
|
|
|
win := subjectStats.TeamID == pm.MatchResult
|
|
tie := pm.MatchResult == 0
|
|
|
|
if _, ok := matchSeen[pm.ID]; !ok {
|
|
if pm.Map != "" {
|
|
mapMatchTotal[pm.Map]++
|
|
if win {
|
|
mapWins[pm.Map]++
|
|
} else if tie {
|
|
mapTies[pm.Map]++
|
|
}
|
|
}
|
|
|
|
wSs, err := subjectStats.QueryWeaponStats().
|
|
Select(weapon.FieldEqType, weapon.FieldDmg).All(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, weaponStat := range wSs {
|
|
found := false
|
|
for _, dmgS := range mResponse.WeaponDmg {
|
|
if dmgS.Eq == weaponStat.EqType {
|
|
dmgS.Dmg += weaponStat.Dmg
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
mResponse.WeaponDmg = append(mResponse.WeaponDmg, &WeaponDmg{
|
|
Eq: weaponStat.EqType,
|
|
Dmg: weaponStat.Dmg,
|
|
})
|
|
}
|
|
}
|
|
|
|
matchSeen[pm.ID] = true
|
|
}
|
|
|
|
// check if same team
|
|
if subjectStats.TeamID == currentStats.TeamID {
|
|
mostRes.Total++
|
|
if win {
|
|
wins++
|
|
} else if tie {
|
|
ties++
|
|
}
|
|
}
|
|
}
|
|
|
|
if mostRes.Total > 0 {
|
|
mResponse.MostMates = append(mResponse.MostMates, mostRes)
|
|
}
|
|
|
|
if mostRes.Total > 1 && (wins > 0 || ties > 0) {
|
|
mateRes.Player = playerRes
|
|
mateRes.TieRate = float32(ties) / float32(mostRes.Total)
|
|
mateRes.WinRate = float32(wins) / float32(mostRes.Total)
|
|
mateRes.Total = mostRes.Total
|
|
mResponse.BestMates = append(mResponse.BestMates, mateRes)
|
|
}
|
|
}
|
|
|
|
mResponse.TieMaps = map[string]float32{}
|
|
mResponse.WinMaps = map[string]float32{}
|
|
mResponse.TotalMaps = map[string]int{}
|
|
|
|
for tMap, wins := range mapWins {
|
|
mResponse.WinMaps[tMap] = float32(wins) / float32(mapMatchTotal[tMap])
|
|
}
|
|
|
|
for tMap, ties := range mapTies {
|
|
mResponse.TieMaps[tMap] = float32(ties) / float32(mapMatchTotal[tMap])
|
|
}
|
|
|
|
for tMap, total := range mapMatchTotal {
|
|
mResponse.TotalMaps[tMap] = total
|
|
}
|
|
|
|
// sort all results
|
|
sort.Slice(mResponse.BestMates, func(i, j int) bool {
|
|
return mResponse.BestMates[i].WinRate > mResponse.BestMates[j].WinRate
|
|
})
|
|
|
|
sort.Slice(mResponse.MostMates, func(i, j int) bool {
|
|
return mResponse.MostMates[i].Total > mResponse.MostMates[j].Total
|
|
})
|
|
|
|
sort.Slice(mResponse.WeaponDmg, func(i, j int) bool {
|
|
return mResponse.WeaponDmg[i].Dmg > mResponse.WeaponDmg[j].Dmg
|
|
})
|
|
|
|
if len(mResponse.BestMates) > 10 { //nolint:gomnd
|
|
mResponse.BestMates = mResponse.BestMates[:10]
|
|
}
|
|
if len(mResponse.MostMates) > 10 { //nolint:gomnd
|
|
mResponse.MostMates = mResponse.MostMates[:10]
|
|
}
|
|
if len(mResponse.WeaponDmg) > 10 { //nolint:gomnd
|
|
mResponse.WeaponDmg = mResponse.WeaponDmg[:10]
|
|
}
|
|
|
|
return mResponse, nil
|
|
}
|
|
|
|
func DeleteMatch(matchDel *ent.Match, db *ent.Client) error {
|
|
tMatchPlayer, err := matchDel.QueryStats().IDs(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = db.Spray.Delete().Where(spray.HasMatchPlayersWith(matchplayer.IDIn(tMatchPlayer...))).Exec(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = db.Weapon.Delete().Where(weapon.HasStatWith(matchplayer.IDIn(tMatchPlayer...))).Exec(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = db.RoundStats.Delete().Where(roundstats.HasMatchPlayerWith(matchplayer.IDIn(tMatchPlayer...))).Exec(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = db.MatchPlayer.Delete().Where(matchplayer.IDIn(tMatchPlayer...)).Exec(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = db.Match.Delete().Where(match.ID(matchDel.ID)).Exec(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetWinLossTieForPlayer(dbPlayer *ent.Player) (wins, looses, ties int, err error) {
|
|
var res []struct {
|
|
MatchResult int `json:"match_result"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
err = dbPlayer.QueryMatches().GroupBy(match.FieldMatchResult).Aggregate(func(s *sql.Selector) string {
|
|
sT := sql.Table(matchplayer.Table)
|
|
|
|
s.Join(sT).On(s.C(match.FieldID), sT.C(matchplayer.MatchesColumn))
|
|
s.Where(sql.And(
|
|
sql.Or(
|
|
sql.ColumnsEQ(match.FieldMatchResult, matchplayer.FieldTeamID),
|
|
sql.EQ(s.C(match.FieldMatchResult), 0),
|
|
),
|
|
sql.EQ(sT.C(matchplayer.PlayersColumn), dbPlayer.ID),
|
|
))
|
|
return sql.Count("*")
|
|
}).Scan(context.Background(), &res)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
total, err := dbPlayer.QueryMatches().Modify(func(s *sql.Selector) {
|
|
s.Select("COUNT(*)")
|
|
}).Int(context.Background())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, r := range res {
|
|
switch r.MatchResult {
|
|
case 0:
|
|
ties = r.Count
|
|
case 1, 2:
|
|
wins += r.Count
|
|
}
|
|
}
|
|
looses = total - wins - ties
|
|
|
|
return
|
|
}
|
|
|
|
func IsAuthCodeValid(tPlayer *ent.Player, apiKey, shareCode, authCode string, rl *rate.Limiter) (bool, error) {
|
|
var tMatch *ent.Match
|
|
var err error
|
|
if shareCode == "" {
|
|
tMatch, err = tPlayer.QueryMatches().Order(ent.Asc(match.FieldDate)).First(context.Background())
|
|
if err != nil {
|
|
return false, ErrorSharecodeNoMatch
|
|
}
|
|
|
|
_, err := getNextShareCode(tMatch.ShareCode, apiKey, authCode, tPlayer.ID, rl)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
} else {
|
|
_, err := getNextShareCode(shareCode, apiKey, authCode, tPlayer.ID, rl)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
func GetNewShareCodesForPlayer(tPlayer *ent.Player, apiKey string, rl *rate.Limiter) ([]string, error) {
|
|
latestMatch, err := tPlayer.QueryMatches().Order(ent.Desc(match.FieldDate)).First(context.Background())
|
|
if err != nil {
|
|
if ent.IsNotFound(err) {
|
|
return nil, ErrorNoMatch
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
oldestMatch, err := tPlayer.QueryMatches().Order(ent.Asc(match.FieldDate)).First(context.Background())
|
|
if err != nil {
|
|
if ent.IsNotFound(err) {
|
|
return nil, ErrorNoMatch
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var newShareCode *string
|
|
if oldestMatch.ShareCode == tPlayer.OldestSharecodeSeen {
|
|
newShareCode, err = getNextShareCode(latestMatch.ShareCode, apiKey, tPlayer.AuthCode, tPlayer.ID, rl)
|
|
} else {
|
|
newShareCode, err = getNextShareCode(oldestMatch.ShareCode, apiKey, tPlayer.AuthCode, tPlayer.ID, rl)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rCodes []string
|
|
for newShareCode != nil {
|
|
rCodes = append(rCodes, *newShareCode)
|
|
newShareCode, err = getNextShareCode(rCodes[len(rCodes)-1], apiKey, tPlayer.AuthCode, tPlayer.ID, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = tPlayer.Update().SetSharecodeUpdated(time.Now().UTC()).SetOldestSharecodeSeen(oldestMatch.ShareCode).Exec(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rCodes, nil
|
|
}
|
|
|
|
func getNextShareCode(lastCode, apiKey, authCode string, steamID uint64, rl *rate.Limiter) (*string, error) {
|
|
if lastCode == "" || apiKey == "" || authCode == "" || steamID == 0 {
|
|
return nil, fmt.Errorf("invalid arguments")
|
|
}
|
|
|
|
if rl != nil {
|
|
err := rl.Wait(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
log.Debugf("[SC] STEAMPI with %s", fmt.Sprintf(shareCodeURLEntry, "REDACTED", steamID, "REDACTED", lastCode))
|
|
r, err := http.Get(fmt.Sprintf(shareCodeURLEntry, apiKey, steamID, authCode, lastCode)) //nolint:noctx
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch r.StatusCode {
|
|
case http.StatusAccepted:
|
|
return nil, nil
|
|
case http.StatusTooManyRequests:
|
|
return nil, ErrorAuthcodeRateLimit
|
|
case http.StatusServiceUnavailable:
|
|
return nil, ErrorAuthcodeUnavailable
|
|
case http.StatusPreconditionFailed:
|
|
return nil, ErrorSharecodeNoMatch
|
|
case http.StatusForbidden:
|
|
return nil, ErrorAuthcodeUnauthorized
|
|
case http.StatusOK:
|
|
break
|
|
default:
|
|
return nil, fmt.Errorf("temporary steamapi error (HTTP %d)", r.StatusCode)
|
|
}
|
|
|
|
bJSON, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = r.Body.Close()
|
|
|
|
rJSON := new(ShareCodeResponse)
|
|
err = json.Unmarshal(bJSON, rJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &rJSON.Result.Code, nil
|
|
}
|
|
|
|
func Player(db *ent.Client, id interface{}, apiKey string, rl *rate.Limiter) (*ent.Player, error) {
|
|
switch e := id.(type) {
|
|
case uint64:
|
|
return PlayerFromSteamID64(db, e, apiKey, rl)
|
|
case string:
|
|
if SteamID64RegEx.MatchString(e) {
|
|
steamID64, err := strconv.ParseUint(e, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return PlayerFromSteamID64(db, steamID64, apiKey, rl)
|
|
}
|
|
|
|
return PlayerFromVanityURL(db, e, apiKey, rl)
|
|
default:
|
|
return nil, fmt.Errorf("invalid arguments")
|
|
}
|
|
}
|
|
|
|
func PlayerFromVanityURL(db *ent.Client, id, apiKey string, rl *rate.Limiter) (*ent.Player, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("invalid arguments")
|
|
}
|
|
|
|
tPlayer, err := db.Player.Query().Where(player.VanityURL(strings.ToLower(id))).Only(context.Background())
|
|
if err == nil {
|
|
return tPlayer, nil
|
|
} else {
|
|
if rl != nil {
|
|
err := rl.Wait(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
resp, err := steamapi.ResolveVanityURL(id, apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.Success != 1 {
|
|
return nil, fmt.Errorf("vanity url not found")
|
|
}
|
|
|
|
nPlayer, err := PlayerFromSteamID64(db, resp.SteamID, apiKey, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nPlayer, nil
|
|
}
|
|
}
|
|
|
|
func PlayerFromSteamID64(db *ent.Client, steamID uint64, apiKey string, rl *rate.Limiter) (*ent.Player, error) {
|
|
tPlayer, err := db.Player.Get(context.Background(), steamID)
|
|
if err == nil {
|
|
return tPlayer, nil
|
|
} else {
|
|
nPlayer := &ent.Player{ID: steamID}
|
|
uPlayer, err := PlayerFromSteam([]*ent.Player{nPlayer}, nil, apiKey, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(uPlayer) > 0 {
|
|
nPlayer, err = db.Player.Create().
|
|
SetID(steamID).
|
|
SetName(uPlayer[0].Name).
|
|
SetAvatar(uPlayer[0].Avatar).
|
|
SetSteamUpdated(uPlayer[0].SteamUpdated).
|
|
SetVanityURL(uPlayer[0].VanityURL).
|
|
SetVanityURLReal(uPlayer[0].VanityURLReal).
|
|
SetProfileCreated(uPlayer[0].ProfileCreated).
|
|
SetGameBanCount(uPlayer[0].GameBanCount).
|
|
SetVacCount(uPlayer[0].VacCount).
|
|
SetVacDate(uPlayer[0].VacDate).
|
|
SetGameBanDate(uPlayer[0].GameBanDate).
|
|
Save(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nPlayer, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("player %d not found", steamID)
|
|
}
|
|
}
|
|
|
|
func TranslateWithDeepL(text, language, baseURL, apiKey string, timeout int) (translated, detectedLanguage string, err error) {
|
|
c := &http.Client{
|
|
Timeout: time.Duration(timeout) * time.Second,
|
|
}
|
|
v := url.Values{}
|
|
v.Set("auth_key", apiKey)
|
|
v.Set("text", text)
|
|
v.Set("target_lang", language)
|
|
dlResp, err := c.PostForm("https://"+baseURL+"/v2/translate", v) //nolint:noctx
|
|
switch {
|
|
case err != nil:
|
|
return "", "", fmt.Errorf("deepl response: %w", err)
|
|
case dlResp.StatusCode != http.StatusOK:
|
|
return "", "", fmt.Errorf("deepl response %d", dlResp.StatusCode)
|
|
default:
|
|
respBytes, err := io.ReadAll(dlResp.Body)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error reading deepl response: %w", err)
|
|
}
|
|
_ = dlResp.Body.Close()
|
|
dlRespJSON := new(DeepLResponse)
|
|
err = json.Unmarshal(respBytes, &dlRespJSON)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error decoding json from deepl: %w", err)
|
|
}
|
|
return dlRespJSON.Translations[0].Text, strings.ToLower(dlRespJSON.Translations[0].DetectedSourceLanguage), nil
|
|
}
|
|
}
|
|
|
|
func PlayerFromSteam(players []*ent.Player, db *ent.Client, apiKey string, rl *rate.Limiter) ([]*ent.Player, error) {
|
|
var idsToUpdate []uint64
|
|
|
|
for _, updatePlayer := range players {
|
|
idsToUpdate = append(idsToUpdate, updatePlayer.ID)
|
|
}
|
|
|
|
batches := int(math.Round((float64(len(players)) / 1000) + 0.5)) //nolint:gomnd
|
|
|
|
if rl != nil {
|
|
err := rl.WaitN(context.Background(), batches)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
playerSum, err := steamapi.GetPlayerSummaries(idsToUpdate, apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: what happens if a player deleted their profile?
|
|
var nPlayers []*ent.Player
|
|
for _, pS := range playerSum {
|
|
// check for vanityURL
|
|
if SteamID64RegEx.MatchString(path.Base(pS.ProfileURL)) {
|
|
pS.ProfileURL = ""
|
|
} else {
|
|
pS.ProfileURL = path.Base(pS.ProfileURL)
|
|
}
|
|
|
|
var tPlayer *ent.Player
|
|
if db != nil {
|
|
tPlayer, err = db.Player.UpdateOneID(pS.SteamID).
|
|
SetName(pS.PersonaName).
|
|
SetAvatar(pS.AvatarHash).
|
|
SetVanityURL(strings.ToLower(pS.ProfileURL)).
|
|
SetVanityURLReal(pS.ProfileURL).
|
|
SetSteamUpdated(time.Now().UTC()).
|
|
SetProfileCreated(time.Unix(pS.TimeCreated, 0).UTC()).
|
|
Save(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
tPlayer = &ent.Player{
|
|
ID: pS.SteamID,
|
|
Name: pS.PersonaName,
|
|
Avatar: pS.AvatarHash,
|
|
VanityURL: strings.ToLower(pS.ProfileURL),
|
|
VanityURLReal: pS.ProfileURL,
|
|
SteamUpdated: time.Now().UTC(),
|
|
ProfileCreated: time.Unix(pS.TimeCreated, 0),
|
|
}
|
|
}
|
|
nPlayers = append(nPlayers, tPlayer)
|
|
}
|
|
|
|
if rl != nil {
|
|
err := rl.WaitN(context.Background(), batches)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
bans, err := steamapi.GetPlayerBans(idsToUpdate, apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ban := range bans {
|
|
if ban.VACBanned || ban.NumberOfGameBans > 0 {
|
|
banDate := time.Now().UTC().AddDate(0, 0, -1*int(ban.DaysSinceLastBan))
|
|
|
|
if db != nil && ban.VACBanned {
|
|
err := db.Player.UpdateOneID(ban.SteamID).SetVacCount(int(ban.NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if db != nil && ban.NumberOfGameBans > 0 {
|
|
err := db.Player.UpdateOneID(ban.SteamID).SetGameBanCount(int(ban.NumberOfGameBans)).SetGameBanDate(banDate).Exec(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, p := range nPlayers {
|
|
if p.ID == ban.SteamID {
|
|
if ban.NumberOfGameBans > 0 {
|
|
p.GameBanCount = int(ban.NumberOfGameBans)
|
|
p.GameBanDate = banDate
|
|
}
|
|
if ban.VACBanned {
|
|
p.VacCount = int(ban.NumberOfVACBans)
|
|
p.GameBanDate = banDate
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nPlayers, nil
|
|
}
|
|
|
|
func RealIP(header *http.Header, fallback string) string {
|
|
if header.Get("X-Forwarded-For") != "" {
|
|
return header.Get("X-Forwarded-For")
|
|
} else {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
func Rollback(tx *ent.Tx, err error) error {
|
|
if rErr := tx.Rollback(); rErr != nil {
|
|
err = fmt.Errorf("%w: %v", err, rErr)
|
|
}
|
|
return err
|
|
}
|