510 lines
13 KiB
Go
510 lines
13 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"csgowtfd/ent"
|
|
"csgowtfd/ent/match"
|
|
"csgowtfd/ent/player"
|
|
"csgowtfd/ent/stats"
|
|
"encoding/json"
|
|
"entgo.io/ent/dialect/sql"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/an0nfunc/go-steamapi"
|
|
log "github.com/sirupsen/logrus"
|
|
"go.uber.org/ratelimit"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Conf struct {
|
|
Logging struct {
|
|
Level string
|
|
}
|
|
Db struct {
|
|
Driver string
|
|
ConnectTo string `yaml:"connect_to"`
|
|
}
|
|
Parser struct {
|
|
Worker int
|
|
}
|
|
Steam struct {
|
|
Username string
|
|
Password string
|
|
APIKey string `yaml:"api_key"`
|
|
RatePerSecond int `yaml:"rate_per_sec"`
|
|
Sentry string
|
|
LoginKey string `yaml:"login_key"`
|
|
ServerList string `yaml:"server_list"`
|
|
}
|
|
Redis struct {
|
|
Address string
|
|
Password string
|
|
}
|
|
Httpd struct {
|
|
CORSAllowDomains string `yaml:"cors_allow_domains"`
|
|
Listen []struct {
|
|
Socket string
|
|
Host string
|
|
Port int
|
|
}
|
|
}
|
|
}
|
|
|
|
type DBWithLock struct {
|
|
Client *ent.Client
|
|
Lock *sync.RWMutex
|
|
}
|
|
|
|
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 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 HitGroup struct {
|
|
Head uint `json:"head,omitempty"`
|
|
Chest uint `json:"chest,omitempty"`
|
|
Stomach uint `json:"stomach,omitempty"`
|
|
LeftArm uint `json:"left_arm,omitempty"`
|
|
RightArm uint `json:"right_arm,omitempty"`
|
|
LeftLeg uint `json:"left_leg,omitempty"`
|
|
RightLeg uint `json:"right_leg,omitempty"`
|
|
Gear uint `json:"gear,omitempty"`
|
|
}
|
|
|
|
type UD struct {
|
|
HE uint `json:"he,omitempty"`
|
|
Flames uint `json:"flames,omitempty"`
|
|
Flash uint `json:"flash,omitempty"`
|
|
Decoy uint `json:"decoy,omitempty"`
|
|
Smoke uint `json:"smoke,omitempty"`
|
|
}
|
|
|
|
type Damage struct {
|
|
Enemy uint `json:"enemy,omitempty"`
|
|
Team uint `json:"team,omitempty"`
|
|
UD *UD `json:"ud,omitempty"`
|
|
HitGroup *HitGroup `json:"hit_group,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 *time.Time `json:"vac_date,omitempty"`
|
|
Tracked bool `json:"tracked"`
|
|
VanityURL string `json:"vanity_url,omitempty"`
|
|
MatchStats *MatchStats `json:"match_stats,omitempty"`
|
|
Matches []*MatchResponse `json:"matches,omitempty"`
|
|
}
|
|
|
|
type WeaponResponse struct {
|
|
Player *PlayerResponse `json:"player"`
|
|
Eq map[string][][]int `json:"eq,omitempty"`
|
|
}
|
|
|
|
type MatchResponse struct {
|
|
MatchId uint64 `json:"match_id,string"`
|
|
ShareCode string `json:"share_code,omitempty"`
|
|
Map string `json:"map"`
|
|
Date time.Time `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"`
|
|
Stats interface{} `json:"stats,omitempty"`
|
|
}
|
|
|
|
type (
|
|
AuthcodeUnauthorizedError struct {
|
|
error
|
|
}
|
|
AuthcodeRateLimitError struct {
|
|
error
|
|
}
|
|
SharecodeNoMatchError struct {
|
|
error
|
|
}
|
|
)
|
|
|
|
const (
|
|
shareCodeURLEntry = "https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=%s&steamid=%d&steamidkey=%s&knowncode=%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 SendJSON(data interface{}, w http.ResponseWriter) error {
|
|
playerJson, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, err = w.Write(playerJson)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetMatchStats(dbPlayer *ent.Player, lock *sync.RWMutex) (int, int, int, error) {
|
|
var res []struct {
|
|
MatchResult int `json:"match_result"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
lock.RLock()
|
|
err := dbPlayer.QueryMatches().GroupBy(match.FieldMatchResult).Aggregate(func(s *sql.Selector) string {
|
|
sT := sql.Table(stats.Table)
|
|
s.Join(sT).On(s.C(match.FieldID), sT.C(stats.MatchesColumn))
|
|
s.Where(sql.And(sql.Or(sql.ColumnsEQ(match.FieldMatchResult, stats.FieldTeamID), sql.EQ(s.C(match.FieldMatchResult), 0)), sql.EQ(sT.C(stats.PlayersColumn), dbPlayer.ID)))
|
|
return sql.Count("*")
|
|
}).Scan(context.Background(), &res)
|
|
lock.RUnlock()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
lock.RLock()
|
|
total, err := dbPlayer.QueryMatches().Modify(func(s *sql.Selector) {
|
|
s.Select("COUNT(*)")
|
|
}).Int(context.Background())
|
|
lock.RUnlock()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
if len(res) < 1 {
|
|
return 0, 0, 0, nil
|
|
}
|
|
var (
|
|
wins int
|
|
ties int
|
|
)
|
|
|
|
for _, r := range res {
|
|
switch r.MatchResult {
|
|
case 0:
|
|
ties = r.Count
|
|
case 1, 2:
|
|
wins += r.Count
|
|
}
|
|
}
|
|
|
|
return wins, ties, total - wins - ties, nil
|
|
}
|
|
|
|
func IsAuthCodeValid(player *ent.Player, lock *sync.RWMutex, apiKey string, shareCode string, authCode string, rl ratelimit.Limiter) (bool, error) {
|
|
var tMatch *ent.Match
|
|
var err error
|
|
if shareCode == "" {
|
|
lock.RLock()
|
|
tMatch, err = player.QueryMatches().Order(ent.Desc(match.FieldDate)).First(context.Background())
|
|
lock.RUnlock()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
_, err := getNextShareCode(tMatch.ShareCode, apiKey, authCode, player.ID, rl)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
} else {
|
|
_, err := getNextShareCode(shareCode, apiKey, authCode, player.ID, rl)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
func GetNewShareCodesForPlayer(player *ent.Player, lock *sync.RWMutex, apiKey string, rl ratelimit.Limiter) ([]string, error) {
|
|
lock.RLock()
|
|
tMatch, err := player.QueryMatches().Order(ent.Desc(match.FieldDate)).First(context.Background())
|
|
lock.RUnlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var rCodes []string
|
|
newShareCode, err := getNextShareCode(tMatch.ShareCode, apiKey, player.AuthCode, player.ID, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for newShareCode != "n/a" {
|
|
rCodes = append(rCodes, newShareCode)
|
|
newShareCode, err = getNextShareCode(rCodes[len(rCodes)-1], apiKey, player.AuthCode, player.ID, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
|
|
lock.Lock()
|
|
err = player.Update().SetSharecodeUpdated(time.Now().UTC()).Exec(context.Background())
|
|
lock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rCodes, nil
|
|
}
|
|
|
|
func getNextShareCode(lastCode string, apiKey string, authCode string, steamId uint64, rl ratelimit.Limiter) (string, error) {
|
|
if lastCode == "" || apiKey == "" || authCode == "" || steamId == 0 {
|
|
return "", fmt.Errorf("invalid arguments")
|
|
}
|
|
|
|
rl.Take()
|
|
log.Debugf("[SC] STEAMPI with %s", fmt.Sprintf(shareCodeURLEntry, "REDACTED", steamId, "REDACTED", lastCode))
|
|
r, err := http.Get(fmt.Sprintf(shareCodeURLEntry, apiKey, steamId, authCode, lastCode))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch r.StatusCode {
|
|
case http.StatusAccepted:
|
|
return "n/a", nil
|
|
case http.StatusTooManyRequests, http.StatusServiceUnavailable:
|
|
return "", AuthcodeRateLimitError{errors.New("api temp. ratelimited")}
|
|
case http.StatusPreconditionFailed:
|
|
return "", SharecodeNoMatchError{errors.New("sharecode not from player history")}
|
|
case http.StatusForbidden:
|
|
return "", AuthcodeUnauthorizedError{errors.New("authcode unauthorized")}
|
|
case http.StatusOK:
|
|
break
|
|
default:
|
|
return "", errors.New("temporary steamapi error")
|
|
}
|
|
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(r.Body)
|
|
bJson, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rJson := new(shareCodeResponse)
|
|
err = json.Unmarshal(bJson, rJson)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return rJson.Result.Code, nil
|
|
}
|
|
|
|
func GetPlayer(db *DBWithLock, id interface{}, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) {
|
|
switch e := id.(type) {
|
|
case uint64:
|
|
return GetPlayerFromSteamID64(db, e, apiKey, rl)
|
|
case string:
|
|
if SteamId64RegEx.MatchString(e) {
|
|
steamID64, err := strconv.ParseUint(e, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return GetPlayerFromSteamID64(db, steamID64, apiKey, rl)
|
|
}
|
|
|
|
return GetPlayerFromVanityURL(db, e, apiKey, rl)
|
|
default:
|
|
return nil, fmt.Errorf("invalid arguments")
|
|
}
|
|
}
|
|
|
|
func GetPlayerFromVanityURL(db *DBWithLock, id string, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("invalid arguments")
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tPlayer, err := db.Client.Player.Query().Where(player.VanityURL(strings.ToLower(id))).Only(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err == nil {
|
|
return tPlayer, nil
|
|
} else {
|
|
rl.Take()
|
|
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 := GetPlayerFromSteamID64(db, resp.SteamID, apiKey, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nPlayer, nil
|
|
}
|
|
}
|
|
|
|
func GetPlayerFromSteamID64(db *DBWithLock, steamID uint64, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) {
|
|
db.Lock.RLock()
|
|
tPlayer, err := db.Client.Player.Get(context.Background(), steamID)
|
|
db.Lock.RUnlock()
|
|
if err == nil {
|
|
return tPlayer, nil
|
|
} else {
|
|
db.Lock.Lock()
|
|
nPlayer, err := db.Client.Player.Create().SetID(steamID).Save(context.Background())
|
|
db.Lock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uPlayer, err := UpdatePlayerFromSteam([]*ent.Player{nPlayer}, db.Client, apiKey, db.Lock, rl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(uPlayer) > 0 {
|
|
return uPlayer[0], nil
|
|
} else {
|
|
return nil, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func UpdatePlayerFromSteam(players []*ent.Player, db *ent.Client, apiKey string, lock *sync.RWMutex, rl ratelimit.Limiter) ([]*ent.Player, error) {
|
|
var idsToUpdate []uint64
|
|
|
|
for _, updatePlayer := range players {
|
|
idsToUpdate = append(idsToUpdate, updatePlayer.ID)
|
|
}
|
|
|
|
log.Infof("[STEAM] Getting %d profiles from steam", len(idsToUpdate))
|
|
|
|
rl.Take()
|
|
playerSum, err := steamapi.GetPlayerSummaries(idsToUpdate, apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var nPlayers []*ent.Player
|
|
for _, pS := range playerSum {
|
|
// TODO: what happens if a player deleted their profile?
|
|
|
|
// check for vanityURL
|
|
if SteamId64RegEx.MatchString(path.Base(pS.ProfileURL)) {
|
|
pS.ProfileURL = ""
|
|
} else {
|
|
pS.ProfileURL = path.Base(pS.ProfileURL)
|
|
}
|
|
|
|
lock.Lock()
|
|
tPlayer, err := db.Player.UpdateOneID(pS.SteamID).
|
|
SetName(pS.PersonaName).
|
|
SetAvatar(pS.AvatarHash).
|
|
SetSteamUpdated(time.Now().UTC()).
|
|
SetVanityURL(strings.ToLower(pS.ProfileURL)).
|
|
SetVanityURLReal(pS.ProfileURL).
|
|
SetSteamUpdated(time.Now().UTC()).
|
|
SetProfileCreated(time.Unix(pS.TimeCreated, 0).UTC()).
|
|
Save(context.Background())
|
|
lock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nPlayers = append(nPlayers, tPlayer)
|
|
}
|
|
|
|
rl.Take()
|
|
bans, err := steamapi.GetPlayerBans(idsToUpdate, apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ban := range bans {
|
|
if ban.NumberOfVACBans > 0 {
|
|
banDate := time.Now().UTC().AddDate(0, 0, -1*int(bans[0].DaysSinceLastBan))
|
|
|
|
lock.Lock()
|
|
err := db.Player.UpdateOneID(ban.SteamID).SetVacCount(int(ban.NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background())
|
|
lock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nPlayers, nil
|
|
}
|