815 lines
22 KiB
Go
815 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"csgowtfd/csgo"
|
|
"csgowtfd/ent"
|
|
"csgowtfd/ent/match"
|
|
"csgowtfd/ent/migrate"
|
|
"csgowtfd/ent/player"
|
|
"csgowtfd/ent/stats"
|
|
"csgowtfd/utils"
|
|
"entgo.io/ent/dialect/sql"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/go-redis/cache/v8"
|
|
"github.com/go-redis/redis/v8"
|
|
"github.com/gorilla/handlers"
|
|
"github.com/gorilla/mux"
|
|
_ "github.com/jackc/pgx/v4/stdlib"
|
|
"github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/wercker/journalhook"
|
|
"go.uber.org/ratelimit"
|
|
"gopkg.in/yaml.v3"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
conf = utils.Conf{}
|
|
demoLoader = &csgo.DemoMatchLoader{}
|
|
router *mux.Router
|
|
db *utils.DBWithLock
|
|
rdb *redis.Client
|
|
rdc *cache.Cache
|
|
firstHK = true
|
|
rL ratelimit.Limiter
|
|
configFlag = flag.String("config", "config.yaml", "Set config to use")
|
|
authCodeFlag = flag.String("authcode", "", "Provide Steam AuthCode to login")
|
|
journalLogFlag = flag.Bool("journal", false, "Log to systemd journal instead of stdout")
|
|
)
|
|
|
|
func housekeeping() {
|
|
for {
|
|
if !firstHK {
|
|
time.Sleep(5 * time.Minute)
|
|
}
|
|
firstHK = false
|
|
|
|
// update players from steam
|
|
db.Lock.RLock()
|
|
tPlayerNeedSteamUpdate, err := db.Client.Player.Query().Where(
|
|
player.SteamUpdatedLTE(time.Now().UTC().AddDate(0, 0, -1)),
|
|
).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Errorf("[HK] Can't query players: %v", err)
|
|
continue
|
|
}
|
|
|
|
_, err = utils.UpdatePlayerFromSteam(tPlayerNeedSteamUpdate, db.Client, conf.Steam.APIKey, db.Lock, rL)
|
|
|
|
// getting new sharecodes
|
|
if !demoLoader.GCReady {
|
|
log.Warningf("[HK] GC not ready, skipping sharecode refresh")
|
|
continue
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tPlayerNeedShareCodeUpdate, err := db.Client.Player.Query().Where(
|
|
player.And(
|
|
player.Or(
|
|
player.SharecodeUpdatedLTE(time.Now().UTC().Add(time.Duration(-30)*time.Minute)),
|
|
player.SharecodeUpdatedIsNil(),
|
|
),
|
|
player.Not(player.AuthCodeIsNil()),
|
|
)).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Errorf("[HK] Can't query players: %v", err)
|
|
continue
|
|
}
|
|
|
|
for _, tPlayer := range tPlayerNeedShareCodeUpdate {
|
|
shareCodes, err := utils.GetNewShareCodesForPlayer(tPlayer, db.Lock, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case utils.AuthcodeUnauthorizedError:
|
|
log.Infof("[HK] authCode for player %d is no longer valid", tPlayer.ID)
|
|
db.Lock.Lock()
|
|
err = tPlayer.Update().ClearAuthCode().ClearSharecodeUpdated().Exec(context.Background())
|
|
db.Lock.Unlock()
|
|
if err != nil {
|
|
log.Errorf("[HK] Unable to clear authcode for player %d: %v", tPlayer.ID, err)
|
|
}
|
|
continue
|
|
case utils.SharecodeNoMatchError:
|
|
log.Warningf("[HK] last shareCode for player %d does not match player", tPlayer.ID)
|
|
continue
|
|
default:
|
|
log.Errorf("[HK] Error while request sharecodes: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
for _, code := range shareCodes {
|
|
err := demoLoader.LoadDemo(&csgo.Demo{
|
|
ShareCode: code,
|
|
})
|
|
if err != nil {
|
|
log.Warningf("[HK] Failure to queue match: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// try parsing demos not parsed
|
|
db.Lock.RLock()
|
|
tMatches, err := db.Client.Match.Query().Where(match.And(match.DateGT(time.Now().UTC().AddDate(0, 0, -30)), match.DemoParsed(false))).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Warningf("[HK] Failure getting matches to retry parsing: %v", err)
|
|
continue
|
|
}
|
|
|
|
for _, m := range tMatches {
|
|
log.Infof("[HK] Try reparsing match %d, played on %s", m.ID, m.Date)
|
|
err := demoLoader.LoadDemo(&csgo.Demo{MatchId: m.ID, ShareCode: m.ShareCode})
|
|
if err != nil {
|
|
log.Warningf("[HK] Failure trying to parse match %d: %v", m.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getPlayer(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
id := mux.Vars(r)["id"]
|
|
tPlayer, err := utils.GetPlayer(db, id, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
log.Infof("[GP] Player not found: %+v", err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
response := utils.PlayerResponse{
|
|
SteamID64: tPlayer.ID,
|
|
Name: tPlayer.Name,
|
|
Avatar: tPlayer.Avatar,
|
|
VAC: tPlayer.Vac,
|
|
VanityURL: tPlayer.VanityURLReal,
|
|
Tracked: tPlayer.AuthCode != "",
|
|
Matches: []*utils.MatchResponse{},
|
|
}
|
|
|
|
if !tPlayer.VacDate.IsZero() {
|
|
response.VACDate = &tPlayer.VacDate
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tMatches, err := tPlayer.QueryMatches().Order(ent.Desc(match.FieldDate)).Limit(10).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil || len(tMatches) == 0 {
|
|
log.Debugf("[GP] No matches found for player %s", id)
|
|
err := utils.SendJSON(response, w)
|
|
if err != nil {
|
|
log.Errorf("[GP] Unable to marshal JSON: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
metaStats := new(utils.MatchStats)
|
|
err = rdc.Get(context.Background(), fmt.Sprintf("csgowtfd_meta_%d", tPlayer.ID), &metaStats)
|
|
if err != nil {
|
|
wins, ties, losses, err := utils.GetMatchStats(tPlayer, db.Lock)
|
|
if err != nil {
|
|
log.Errorf("[GP] Error retrieving match-stats for player %s: %v", id, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response.MatchStats.Win = wins
|
|
response.MatchStats.Tie = ties
|
|
response.MatchStats.Loss = losses
|
|
|
|
err = rdc.Set(&cache.Item{
|
|
Ctx: context.Background(),
|
|
Key: fmt.Sprintf("csgowtfd_meta_%d", tPlayer.ID),
|
|
Value: response.MatchStats,
|
|
TTL: time.Hour * 12,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("[GP] Failure saving to cache: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
log.Debugf("[GP] Metastats for %d saved to cache", tPlayer.ID)
|
|
} else {
|
|
log.Debugf("[GP] Metastats for %d from redis", tPlayer.ID)
|
|
response.MatchStats.Win = metaStats.Win
|
|
response.MatchStats.Tie = metaStats.Tie
|
|
response.MatchStats.Loss = metaStats.Loss
|
|
}
|
|
|
|
for _, iMatch := range tMatches {
|
|
mResponse := &utils.MatchResponse{
|
|
MatchId: iMatch.ID,
|
|
Map: iMatch.Map,
|
|
Date: iMatch.Date,
|
|
Score: [2]int{iMatch.ScoreTeamA, iMatch.ScoreTeamB},
|
|
Duration: iMatch.Duration,
|
|
MatchResult: iMatch.MatchResult,
|
|
MaxRounds: iMatch.MaxRounds,
|
|
Parsed: iMatch.DemoParsed,
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tStats, err := iMatch.QueryStats().Modify(func(s *sql.Selector) {
|
|
s.Select(stats.FieldTeamID, stats.FieldKills, stats.FieldDeaths, stats.FieldAssists, stats.FieldHeadshot,
|
|
stats.FieldMvp, stats.FieldScore, stats.FieldMk2, stats.FieldMk3, stats.FieldMk4, stats.FieldMk5,
|
|
stats.FieldRankOld, stats.FieldRankNew, stats.FieldDmgTeam, stats.FieldDmgEnemy)
|
|
s.Where(sql.EQ(s.C(stats.PlayersColumn), tPlayer.ID))
|
|
}).Only(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
response.Matches = append(response.Matches, mResponse)
|
|
continue
|
|
}
|
|
|
|
sResponse := &utils.StatsResponse{
|
|
TeamID: tStats.TeamID,
|
|
Kills: tStats.Kills,
|
|
Deaths: tStats.Deaths,
|
|
Assists: tStats.Assists,
|
|
Headshot: tStats.Headshot,
|
|
MVP: tStats.Mvp,
|
|
Score: tStats.Score,
|
|
}
|
|
|
|
sResponse.MultiKills = &utils.MultiKills{
|
|
Duo: tStats.Mk2,
|
|
Triple: tStats.Mk3,
|
|
Quad: tStats.Mk4,
|
|
Pent: tStats.Mk5,
|
|
}
|
|
|
|
sResponse.Rank = &utils.Rank{
|
|
Old: tStats.RankOld,
|
|
New: tStats.RankNew,
|
|
}
|
|
|
|
sResponse.Dmg = &utils.Damage{
|
|
Enemy: tStats.DmgEnemy,
|
|
Team: tStats.DmgTeam,
|
|
}
|
|
|
|
mResponse.Stats = sResponse
|
|
response.Matches = append(response.Matches, mResponse)
|
|
}
|
|
|
|
err = utils.SendJSON(response, w)
|
|
if err != nil {
|
|
log.Errorf("[GP] Unable to marshal JSON: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func deletePlayerTrack(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Infof("[DPT] Unable to parse form data: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
id := mux.Vars(r)["id"]
|
|
authCode := r.Form.Get("authcode")
|
|
|
|
if id == "" || authCode == "" || !utils.AuthCodeRegEx.MatchString(authCode) {
|
|
log.Infof("[PPTM] invalid arguments: %+v, %+v", id, authCode)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tPlayer, err := utils.GetPlayer(db, id, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
log.Infof("[PPT] player not found: %+v", err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, err = utils.IsAuthCodeValid(tPlayer, db.Lock, conf.Steam.APIKey, "", authCode, rL)
|
|
if err != nil {
|
|
switch e := err.(type) {
|
|
case utils.AuthcodeUnauthorizedError:
|
|
log.Infof("[DPT] authCode provided for player %s is invalid: %v", id, e)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
default:
|
|
log.Infof("[DPT] Temporary Steam-API problem: %v", e)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
db.Lock.Lock()
|
|
err = tPlayer.Update().ClearAuthCode().ClearSharecodeUpdated().Exec(context.Background())
|
|
db.Lock.Unlock()
|
|
if err != nil {
|
|
log.Warningf("[PPT] update player failed: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func postPlayerTrack(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Infof("[PPT] Unable to parse form data: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
id := mux.Vars(r)["id"]
|
|
authCode := r.Form.Get("authcode")
|
|
shareCode := r.Form.Get("sharecode")
|
|
|
|
if id == "" || authCode == "" || !utils.AuthCodeRegEx.MatchString(authCode) {
|
|
log.Infof("[PPT] invalid arguments: %+v, %+v, %+v", id, authCode, shareCode)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tPlayer, err := utils.GetPlayer(db, id, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
log.Infof("[PPT] player not found: %+v", err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, err = utils.IsAuthCodeValid(tPlayer, db.Lock, conf.Steam.APIKey, shareCode, authCode, rL)
|
|
if err != nil {
|
|
switch e := err.(type) {
|
|
case utils.AuthcodeUnauthorizedError:
|
|
log.Infof("[PPT] authCode provided for player %s is invalid: %v", id, e)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
case utils.SharecodeNoMatchError:
|
|
log.Infof("[PPT] shareCode provided for player %s is invalid: %v", id, e)
|
|
w.WriteHeader(http.StatusPreconditionFailed)
|
|
return
|
|
default:
|
|
log.Infof("[PPT] Temporary Steam-API problem: %v", e)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
db.Lock.Lock()
|
|
err = tPlayer.Update().SetAuthCode(authCode).Exec(context.Background())
|
|
db.Lock.Unlock()
|
|
if err != nil {
|
|
log.Warningf("[PPT] update player failed: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if shareCode != "" && utils.ShareCodeRegEx.MatchString(shareCode) {
|
|
err := demoLoader.LoadDemo(&csgo.Demo{ShareCode: shareCode})
|
|
if err != nil {
|
|
log.Warningf("[PPT] unable to queue match: %v", err)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
func getMatchParse(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
shareCode := mux.Vars(r)["sharecode"]
|
|
|
|
if shareCode == "" || !utils.ShareCodeRegEx.MatchString(shareCode) {
|
|
log.Infof("[PPTM] invalid arguments: %s", shareCode)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err := demoLoader.LoadDemo(&csgo.Demo{
|
|
ShareCode: shareCode,
|
|
})
|
|
if err != nil {
|
|
log.Warningf("[PPTM] unable to queue match: %v", err)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
func getMatchRounds(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
id := mux.Vars(r)["id"]
|
|
|
|
if id == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchId, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMR] Error parsing matchID %s: %v", id, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tStats, err := db.Client.Stats.Query().Where(stats.HasMatchesWith(match.ID(matchId))).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Infof("[GMR] match %d not found: %+v", matchId, err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
resp := map[uint]map[string][]uint{}
|
|
|
|
for _, stat := range tStats {
|
|
db.Lock.RLock()
|
|
tRoundStats, err := stat.QueryRoundStats().All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Warningf("[GMR] Unable to get RoundStats for player %d: %v", stat.PlayerStats, err)
|
|
continue
|
|
}
|
|
|
|
for _, rStat := range tRoundStats {
|
|
if _, ok := resp[rStat.Round]; !ok {
|
|
resp[rStat.Round] = map[string][]uint{}
|
|
}
|
|
|
|
resp[rStat.Round][strconv.FormatUint(stat.PlayerStats, 10)] = []uint{rStat.Equipment, rStat.Spent, rStat.Bank}
|
|
}
|
|
}
|
|
|
|
err = utils.SendJSON(resp, w)
|
|
if err != nil {
|
|
log.Errorf("[GMR] JSON: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func getMatchWeapons(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
id := mux.Vars(r)["id"]
|
|
|
|
if id == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchId, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMW] Error parsing matchID %s: %v", id, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tStats, err := db.Client.Stats.Query().Where(stats.HasMatchesWith(match.ID(matchId))).All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Infof("[GMW] match %d not found: %+v", matchId, err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
mResponse := struct {
|
|
EquipmentMap map[int]string `json:"equipment_map,omitempty"`
|
|
Stats []map[string]map[string][][]int `json:"stats,omitempty"`
|
|
}{
|
|
EquipmentMap: map[int]string{},
|
|
Stats: []map[string]map[string][][]int{},
|
|
}
|
|
|
|
for _, stat := range tStats {
|
|
db.Lock.RLock()
|
|
mWs, err := stat.QueryWeaponStats().All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Warningf("[GMW] Unable to get WeaponStats for player %d: %v", stat.PlayerStats, err)
|
|
continue
|
|
}
|
|
|
|
mWr := map[string]map[string][][]int{}
|
|
playerId := strconv.FormatUint(stat.PlayerStats, 10)
|
|
|
|
for _, wr := range mWs {
|
|
if _, exists := mWr[strconv.FormatUint(stat.PlayerStats, 10)]; !exists {
|
|
mWr[playerId] = map[string][][]int{}
|
|
}
|
|
|
|
victim := strconv.FormatUint(wr.Victim, 10)
|
|
mWr[playerId][victim] = append(mWr[playerId][victim], []int{wr.EqType, wr.HitGroup, int(wr.Dmg)})
|
|
|
|
if _, exist := mResponse.EquipmentMap[wr.EqType]; !exist {
|
|
mResponse.EquipmentMap[wr.EqType] = common.EquipmentType(wr.EqType).String()
|
|
}
|
|
}
|
|
mResponse.Stats = append(mResponse.Stats, mWr)
|
|
}
|
|
|
|
err = utils.SendJSON(mResponse, w)
|
|
if err != nil {
|
|
log.Errorf("[GMW] JSON: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func getMatch(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", conf.Httpd.CORSAllowDomains)
|
|
id := mux.Vars(r)["id"]
|
|
|
|
if id == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchId, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GM] Unable to parse matchID %s: %v", id, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tMatch, err := db.Client.Match.Query().Where(match.ID(matchId)).Only(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Infof("[GM] match %d not found: %v", matchId, err)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
mResponse := &utils.MatchResponse{
|
|
MatchId: tMatch.ID,
|
|
ShareCode: tMatch.ShareCode,
|
|
Map: tMatch.Map,
|
|
Date: tMatch.Date,
|
|
Score: [2]int{tMatch.ScoreTeamA, tMatch.ScoreTeamB},
|
|
Duration: tMatch.Duration,
|
|
MatchResult: tMatch.MatchResult,
|
|
MaxRounds: tMatch.MaxRounds,
|
|
Parsed: tMatch.DemoParsed,
|
|
Stats: []*utils.StatsResponse{},
|
|
}
|
|
|
|
db.Lock.RLock()
|
|
tStats, err := tMatch.QueryStats().WithPlayers().All(context.Background())
|
|
db.Lock.RUnlock()
|
|
if err != nil {
|
|
log.Errorf("[GM] Unable to find stats for match %d: %v", tMatch.ID, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
tmpStats := make([]*utils.StatsResponse, 0)
|
|
|
|
for _, iStats := range tStats {
|
|
sResponse := &utils.StatsResponse{
|
|
Player: utils.PlayerResponse{
|
|
SteamID64: iStats.Edges.Players.ID,
|
|
Name: iStats.Edges.Players.Name,
|
|
Avatar: iStats.Edges.Players.Avatar,
|
|
VAC: iStats.Edges.Players.Vac,
|
|
VanityURL: iStats.Edges.Players.VanityURLReal,
|
|
Tracked: iStats.Edges.Players.AuthCode != "",
|
|
},
|
|
TeamID: iStats.TeamID,
|
|
Kills: iStats.Kills,
|
|
Deaths: iStats.Deaths,
|
|
Assists: iStats.Assists,
|
|
Headshot: iStats.Headshot,
|
|
MVP: iStats.Mvp,
|
|
Score: iStats.Score,
|
|
|
|
Dmg: &utils.Damage{
|
|
Team: iStats.DmgTeam,
|
|
Enemy: iStats.DmgEnemy,
|
|
UD: &utils.UD{
|
|
HE: iStats.UdHe,
|
|
Smoke: iStats.UdSmoke,
|
|
Flash: iStats.UdFlash,
|
|
Decoy: iStats.UdDecoy,
|
|
Flames: iStats.UdFlames,
|
|
},
|
|
HitGroup: &utils.HitGroup{
|
|
Gear: iStats.HitGroupGear,
|
|
LeftLeg: iStats.HitGroupLeftLeg,
|
|
RightLeg: iStats.HitGroupRightLeg,
|
|
RightArm: iStats.HitGroupRightArm,
|
|
LeftArm: iStats.HitGroupLeftArm,
|
|
Stomach: iStats.HitGroupStomach,
|
|
Chest: iStats.HitGroupChest,
|
|
Head: iStats.HitGroupHead,
|
|
},
|
|
},
|
|
Color: iStats.Color.String(),
|
|
Crosshair: iStats.Crosshair,
|
|
KAST: iStats.Kast,
|
|
Rank: &utils.Rank{
|
|
Old: iStats.RankOld,
|
|
New: iStats.RankNew,
|
|
},
|
|
Flash: &utils.Flash{
|
|
Total: &utils.SelfTeamEnemy{
|
|
Enemy: iStats.FlashTotalEnemy,
|
|
Team: iStats.FlashTotalTeam,
|
|
Self: iStats.FlashTotalSelf,
|
|
},
|
|
Duration: &utils.SelfTeamEnemy{
|
|
Enemy: iStats.FlashDurationEnemy,
|
|
Team: iStats.FlashDurationTeam,
|
|
Self: iStats.FlashDurationSelf,
|
|
},
|
|
},
|
|
MultiKills: &utils.MultiKills{
|
|
Duo: iStats.Mk2,
|
|
Triple: iStats.Mk3,
|
|
Quad: iStats.Mk4,
|
|
Pent: iStats.Mk5,
|
|
},
|
|
}
|
|
|
|
if !iStats.Edges.Players.VacDate.IsZero() {
|
|
switch s := sResponse.Player.(type) {
|
|
case utils.PlayerResponse:
|
|
s.VACDate = &iStats.Edges.Players.VacDate
|
|
}
|
|
}
|
|
|
|
tmpStats = append(tmpStats, sResponse)
|
|
}
|
|
mResponse.Stats = tmpStats
|
|
|
|
err = utils.SendJSON(mResponse, w)
|
|
if err != nil {
|
|
log.Errorf("[GM] JSON: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
/*
|
|
/player/<id> GET player details (last 10 matches)
|
|
/player/<id>/track POST Track player FORM_DATA: authcode, [sharecode]
|
|
/player/<id>/track DELETE Stop tracking player FORM_DATA: authcode
|
|
/match/<id> GET details for match <id>
|
|
/match/<id>/weapons GET weapon-stats for match <id>
|
|
/match/<id>/rounds GET round-stats for match <id>
|
|
/match/parse/<sharecode> GET parses sharecode provided
|
|
*/
|
|
func main() {
|
|
killSignals := make(chan os.Signal, 1)
|
|
signal.Notify(killSignals, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
flag.Parse()
|
|
|
|
confStr, err := os.ReadFile(*configFlag)
|
|
if err != nil {
|
|
log.Fatalf("Unable to open config: %v", err)
|
|
}
|
|
|
|
err = yaml.Unmarshal(confStr, &conf)
|
|
if err != nil {
|
|
log.Fatalf("Unable to parse config: %v", err)
|
|
}
|
|
|
|
lvl, err := log.ParseLevel(conf.Logging.Level)
|
|
if err != nil {
|
|
log.Fatalf("Failure setting logging level: %v", err)
|
|
}
|
|
log.SetLevel(lvl)
|
|
if *journalLogFlag {
|
|
journalhook.Enable()
|
|
}
|
|
|
|
db = &utils.DBWithLock{
|
|
Lock: new(sync.RWMutex),
|
|
}
|
|
|
|
if conf.Db.Driver == "pgx" {
|
|
pdb, err := sql.Open("pgx", conf.Db.ConnectTo)
|
|
if err != nil {
|
|
log.Panicf("Failed to open database %s: %v", conf.Db.ConnectTo, err)
|
|
}
|
|
|
|
defer func(db *sql.Driver) {
|
|
_ = db.Close()
|
|
}(pdb)
|
|
|
|
drv := sql.OpenDB("postgres", pdb.DB())
|
|
db.Client = ent.NewClient(ent.Driver(drv))
|
|
} else {
|
|
db.Client, err = ent.Open(conf.Db.Driver, conf.Db.ConnectTo)
|
|
if err != nil {
|
|
log.Panicf("Failed to open database %s: %v", conf.Db.ConnectTo, err)
|
|
}
|
|
defer func(Client *ent.Client) {
|
|
_ = Client.Close()
|
|
}(db.Client)
|
|
}
|
|
|
|
if err := db.Client.Schema.Create(
|
|
context.Background(),
|
|
migrate.WithDropIndex(true),
|
|
migrate.WithDropColumn(true),
|
|
); err != nil {
|
|
log.Panicf("Automigrate failed: %v", err)
|
|
}
|
|
|
|
rdb = redis.NewClient(&redis.Options{
|
|
Addr: conf.Redis.Address,
|
|
Password: conf.Redis.Password,
|
|
DB: 0,
|
|
})
|
|
|
|
rdc = cache.New(&cache.Options{
|
|
Redis: rdb,
|
|
LocalCache: cache.NewTinyLFU(1000, time.Minute),
|
|
})
|
|
|
|
rL = ratelimit.New(conf.Steam.RatePerSecond)
|
|
|
|
// setup GC
|
|
err = demoLoader.Setup(&csgo.DemoMatchLoaderConfig{
|
|
Username: conf.Steam.Username,
|
|
Password: conf.Steam.Password,
|
|
AuthCode: *authCodeFlag,
|
|
Sentry: conf.Steam.Sentry,
|
|
LoginKey: conf.Steam.LoginKey,
|
|
ServerList: conf.Steam.ServerList,
|
|
Db: db.Client,
|
|
Lock: db.Lock,
|
|
Worker: conf.Parser.Worker,
|
|
ApiKey: conf.Steam.APIKey,
|
|
RateLimit: rL,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Unbale to setup DemoLoader: %v", err)
|
|
}
|
|
|
|
// start housekeeper
|
|
go housekeeping()
|
|
|
|
// routes
|
|
router = mux.NewRouter().StrictSlash(true)
|
|
router.HandleFunc("/player/{id}", getPlayer).Methods(http.MethodGet, http.MethodOptions)
|
|
router.HandleFunc("/player/{id}/track", postPlayerTrack).Methods(http.MethodPost, http.MethodOptions)
|
|
router.HandleFunc("/player/{id}/track", deletePlayerTrack).Methods(http.MethodOptions, http.MethodDelete)
|
|
router.HandleFunc("/match/parse/{sharecode}", getMatchParse).Methods(http.MethodGet, http.MethodOptions)
|
|
router.HandleFunc("/match/{id:[0-9]{19}}", getMatch).Methods(http.MethodGet, http.MethodOptions)
|
|
router.HandleFunc("/match/{id:[0-9]{19}}/weapons", getMatchWeapons).Methods(http.MethodGet, http.MethodOptions)
|
|
router.HandleFunc("/match/{id:[0-9]{19}}/rounds", getMatchRounds).Methods(http.MethodGet, http.MethodOptions)
|
|
router.Use(mux.CORSMethodMiddleware(router))
|
|
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
|
|
proxyRouter := handlers.ProxyHeaders(loggedRouter)
|
|
|
|
log.Info("Start listening...")
|
|
|
|
sockets := make([]net.Listener, 0)
|
|
|
|
for _, l := range conf.Httpd.Listen {
|
|
if l.Socket != "" {
|
|
sL, err := net.Listen("unix", l.Socket)
|
|
if err != nil {
|
|
log.Fatalf("Failure listing on socket %s: %v", l.Socket, err)
|
|
}
|
|
sockets = append(sockets, sL)
|
|
go func() {
|
|
_ = http.Serve(sL, proxyRouter)
|
|
}()
|
|
} else {
|
|
tL, err := net.Listen("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port))
|
|
if err != nil {
|
|
log.Fatalf("Failure listing on %s:%d: %v", l.Host, l.Port, err)
|
|
}
|
|
go func() {
|
|
err = http.Serve(tL, proxyRouter)
|
|
if err != nil {
|
|
log.Fatalf("Failure serving on %s:%d: %v", l.Host, l.Port, err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
<-killSignals
|
|
|
|
for _, s := range sockets {
|
|
_ = s.Close()
|
|
}
|
|
}
|