Files
csgowtfd/utils/utils.go

387 lines
9.5 KiB
Go

package utils
import (
"context"
"csgowtfd/ent"
"csgowtfd/ent/match"
"csgowtfd/ent/player"
"csgowtfd/ent/stats"
"encoding/json"
"encoding/xml"
"entgo.io/ent/dialect/sql"
"fmt"
"github.com/Philipp15b/go-steamapi"
log "github.com/sirupsen/logrus"
"go.uber.org/ratelimit"
"io"
"io/ioutil"
"net/http"
"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"`
}
const (
steamID64Entry = "https://steamcommunity.com/profiles/%d?xml=1"
steamVanityURLEntry = "https://steamcommunity.com/id/%s?xml=1"
shareCodeURLEntry = "https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=%s&steamid=%d&steamidkey=%s&knowncode=%s"
)
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(db *ent.Client, lock *sync.RWMutex, dbPlayer *ent.Player) (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
}
if r.StatusCode == 202 {
return "n/a", nil
} else if r.StatusCode != 200 {
return "", fmt.Errorf("bad response from steam api (HTTP %d)", r.StatusCode)
}
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)
default:
return nil, fmt.Errorf("invalid arguments")
}
}
func GetPlayerFromVanityURL(db *DBWithLock, id string) (*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 {
profile, err := SteamProfile2XML(id, 0)
if err != nil {
return nil, err
}
if profile.Error != "" {
return nil, fmt.Errorf("profile not found")
}
db.Lock.Lock()
nPlayer, err := db.Client.Player.Create().SetID(profile.SteamID64).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetName(profile.ProfileName).Save(context.Background())
db.Lock.Unlock()
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.Query().Where(player.ID(steamID)).Only(context.Background())
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
}
nPlayer, err = UpdatePlayerFromSteam(nPlayer, apiKey, db.Lock, rl)
if err != nil {
return nil, err
}
return nPlayer, nil
}
}
func SteamProfile2XML(id string, steamID64 uint64) (*CommunityXML, error) {
var r *http.Response
var err error
if steamID64 != 0 {
r, err = http.Get(fmt.Sprintf(steamID64Entry, steamID64))
} else {
r, err = http.Get(fmt.Sprintf(steamVanityURLEntry, id))
}
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
cXML := &CommunityXML{}
err = xml.Unmarshal(body, cXML)
if err != nil {
return nil, err
}
return cXML, nil
}
func UpdatePlayerFromSteam(player *ent.Player, apiKey string, lock *sync.RWMutex, rl ratelimit.Limiter) (*ent.Player, error) {
profile, err := SteamProfile2XML("", player.ID)
if err != nil {
return nil, err
}
if profile.Error != "" {
return nil, fmt.Errorf("profile not found")
}
lock.Lock()
tPlayer, err := player.Update().SetName(profile.ProfileName).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetSteamUpdated(time.Now().UTC()).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVanityURLReal(profile.VanityURL).SetSteamUpdated(time.Now().UTC()).Save(context.Background())
lock.Unlock()
if err != nil {
return nil, err
}
rl.Take()
bans, err := steamapi.GetPlayerBans([]uint64{profile.SteamID64}, apiKey)
if err != nil {
return nil, err
}
if len(bans) > 0 && bans[0].NumberOfVACBans > 0 {
banDate := time.Now().UTC().AddDate(0, 0, -1*int(bans[0].DaysSinceLastBan))
lock.Lock()
err := tPlayer.Update().SetVacCount(int(bans[0].NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background())
lock.Unlock()
if err != nil {
return nil, err
}
}
return tPlayer, nil
}