Files
csgowtfd/main.go

1033 lines
30 KiB
Go

package main
import (
"bytes"
"context"
"csgowtfd/csgo"
"csgowtfd/ent"
"csgowtfd/ent/match"
"csgowtfd/ent/matchplayer"
"csgowtfd/ent/migrate"
"csgowtfd/ent/player"
"csgowtfd/utils"
"encoding/gob"
"entgo.io/ent/dialect"
"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"
log "github.com/sirupsen/logrus"
"github.com/wercker/journalhook"
"go.uber.org/ratelimit"
"gopkg.in/yaml.v3"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)
//goland:noinspection ALL
var (
conf = utils.Conf{}
demoLoader = &csgo.DemoMatchLoader{}
router *mux.Router
db *ent.Client
rdb *redis.Client
rdc *cache.Cache
rL ratelimit.Limiter
configFlag = flag.String("config", "config.yaml", "Set config file to use")
journalLogFlag = flag.Bool("journal", false, "Log to systemd journal instead of stdout")
sqlDebugFlag = flag.Bool("sqldebug", false, "Debug SQL queries")
)
func housekeeping() {
for {
time.Sleep(5 * time.Minute)
dur, err := time.ParseDuration(conf.Csgowtfd.ProfileUpdate)
if err != nil {
log.Warningf("[HK] Unable to parse config option profile_update %s: %v", conf.Csgowtfd.ProfileUpdate, err)
dur, _ = time.ParseDuration("168h")
}
// update players from steam
tPlayerNeedSteamUpdate, err := db.Player.Query().Where(
player.SteamUpdatedLTE(time.Now().UTC().Add(dur * -1)),
).All(context.Background())
if err != nil {
log.Errorf("[HK] Can't query players: %v", err)
continue
}
if len(tPlayerNeedSteamUpdate) > 0 {
log.Infof("[HK] Refreshing %d profiles from steam", len(tPlayerNeedSteamUpdate))
_, err = utils.PlayerFromSteam(tPlayerNeedSteamUpdate, db, conf.Steam.APIKey, rL)
if err != nil {
log.Warningf("[HK] Unable to update profiles from steam: %v", err)
}
}
// mark matches as vac/gameban
bPlayers, err := db.Player.Query().Select(player.FieldID, player.FieldGameBanDate, player.FieldVacDate).Where(player.Or(player.GameBanDateNotNil(), player.VacDateNotNil())).All(context.Background())
if err != nil {
log.Warningf("[HK] Unable to query for banned players: %v", err)
}
for _, bp := range bPlayers {
if !bp.GameBanDate.IsZero() {
err := db.Match.Update().Where(match.And(match.HasPlayersWith(player.ID(bp.ID)), match.DateLTE(bp.GameBanDate.AddDate(0, 0, 30)))).SetGamebanPresent(true).Exec(context.Background())
if err != nil {
log.Warningf("[HK] Unable to set gameban/vac for match: %v", err)
}
}
if !bp.VacDate.IsZero() {
err := db.Match.Update().Where(match.And(match.HasPlayersWith(player.ID(bp.ID)), match.DateLTE(bp.VacDate.AddDate(0, 0, 30)))).SetVacPresent(true).Exec(context.Background())
if err != nil {
log.Warningf("[HK] Unable to set gameban/vac for match: %v", err)
}
}
}
// getting new sharecodes
if !demoLoader.GCReady {
log.Warningf("[HK] GC not ready, skipping sharecode refresh")
continue
}
dur, err = time.ParseDuration(conf.Csgowtfd.SharecodeUpdate)
if err != nil {
log.Warningf("[HK] Unable to parse config option sharecode_update %s: %v", conf.Csgowtfd.SharecodeUpdate, err)
dur, _ = time.ParseDuration("30m")
}
tPlayerNeedShareCodeUpdate, err := db.Player.Query().Where(
player.And(
player.Or(
player.SharecodeUpdatedLTE(time.Now().UTC().Add(dur*-1)),
player.SharecodeUpdatedIsNil(),
),
player.Not(player.AuthCodeIsNil()),
)).All(context.Background())
if err != nil {
log.Errorf("[HK] Can't query players: %v", err)
continue
}
for _, tPlayer := range tPlayerNeedShareCodeUpdate {
shareCodes, err := utils.GetNewShareCodesForPlayer(tPlayer, 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)
err = tPlayer.Update().ClearAuthCode().ClearSharecodeUpdated().Exec(context.Background())
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 requesting sharecodes for %d: %v", tPlayer.ID, 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
tMatches, err := db.Match.Query().Where(match.And(match.DateGT(time.Now().UTC().AddDate(0, 0, -1*conf.Csgowtfd.DemosExpire)), match.DemoParsed(false))).All(context.Background())
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 getPlayerMeta(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
l := mux.Vars(r)["limit"]
var limit int
var err error
if l != "" {
limit, err = strconv.Atoi(l)
if err != nil {
log.Infof("[GPM] limit not an int: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
limit = 4
}
if limit > 10 {
log.Infof("[GPM] limit out of bounds: %d", limit)
w.WriteHeader(http.StatusBadRequest)
return
}
tPlayer, err := utils.Player(db, id, conf.Steam.APIKey, nil)
if err != nil {
log.Infof("[GPM] Player not found: %+v", err)
w.WriteHeader(http.StatusNotFound)
return
}
metaStats := new(utils.MetaStatsResponse)
err = rdc.Get(context.Background(), fmt.Sprintf(utils.SideMetaCacheKey, tPlayer.ID), &metaStats)
if err != nil {
metaStats, err = utils.GetMetaStats(tPlayer)
if err != nil {
log.Infof("[GPM] Unable to get MetaStats: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = rdc.Set(&cache.Item{
Ctx: context.Background(),
Key: fmt.Sprintf(utils.SideMetaCacheKey, tPlayer.ID),
Value: metaStats,
TTL: time.Hour * 24 * 30,
})
if err != nil {
log.Errorf("[GPM] Failure saving to cache: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Debugf("[GPM] SideMetaStats for %d saved to cache", tPlayer.ID)
} else {
log.Debugf("[GPM] SideMetaStats for %d from cache", tPlayer.ID)
}
if len(metaStats.BestMates) > limit {
metaStats.BestMates = metaStats.BestMates[:limit]
}
if len(metaStats.MostMates) > limit {
metaStats.MostMates = metaStats.MostMates[:limit]
}
if len(metaStats.WeaponDmg) > limit {
metaStats.WeaponDmg = metaStats.WeaponDmg[:limit]
}
for _, wD := range metaStats.WeaponDmg {
if _, exist := metaStats.EqMap[wD.Eq]; !exist {
metaStats.EqMap[wD.Eq] = common.EquipmentType(wD.Eq).String()
}
}
for _, p := range append(metaStats.BestMates, metaStats.MostMates...) {
if p.Player.Name == "" {
tP, err := utils.Player(db, p.Player.SteamID64, conf.Steam.APIKey, nil)
if err != nil {
log.Warningf("[GPM] Failure getting player: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
p.Player.Avatar = tP.Avatar
p.Player.Name = tP.Name
p.Player.VAC = !tP.VacDate.IsZero()
p.Player.Tracked = tP.AuthCode != ""
p.Player.GameBan = !tP.GameBanDate.IsZero()
p.Player.VanityURL = tP.VanityURLReal
if !tP.GameBanDate.IsZero() {
p.Player.GameBanDate = tP.GameBanDate.Unix()
}
if !tP.VacDate.IsZero() {
p.Player.VACDate = tP.VacDate.Unix()
}
}
}
err = utils.SendJSON(metaStats, w)
if err != nil {
log.Errorf("[GPM] Unable to marshal JSON: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func getPlayer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
t := mux.Vars(r)["time"]
var offsetTime time.Time
if t != "" {
unixOffset, err := strconv.ParseInt(t, 10, 64)
if err != nil {
log.Infof("[GP] offset not an int: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
offsetTime = time.Unix(unixOffset, 0).UTC()
}
tPlayer, err := utils.Player(db, id, conf.Steam.APIKey, nil)
if err != nil || tPlayer == 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.VacDate.IsZero(),
VACDate: tPlayer.VacDate.Unix(),
GameBan: !tPlayer.GameBanDate.IsZero(),
GameBanDate: tPlayer.GameBanDate.Unix(),
VanityURL: tPlayer.VanityURLReal,
Tracked: tPlayer.AuthCode != "",
MatchStats: &utils.MatchStats{
Win: tPlayer.Wins,
Tie: tPlayer.Ties,
Loss: tPlayer.Looses,
},
Matches: []*utils.MatchResponse{},
}
var tMatches []*ent.Match
if !offsetTime.IsZero() {
tMatches, err = tPlayer.QueryMatches().Where(match.DateLT(offsetTime)).Order(ent.Desc(match.FieldDate)).Limit(10).All(context.Background())
} else {
tMatches, err = tPlayer.QueryMatches().Order(ent.Desc(match.FieldDate)).Limit(10).All(context.Background())
}
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
}
for _, iMatch := range tMatches {
mResponse := &utils.MatchResponse{
MatchId: iMatch.ID,
Map: iMatch.Map,
Date: iMatch.Date.Unix(),
Score: [2]int{iMatch.ScoreTeamA, iMatch.ScoreTeamB},
Duration: iMatch.Duration,
MatchResult: iMatch.MatchResult,
MaxRounds: iMatch.MaxRounds,
Parsed: iMatch.DemoParsed,
VAC: iMatch.VacPresent,
GameBan: iMatch.GamebanPresent,
}
tStats, err := iMatch.QueryStats().Modify(func(s *sql.Selector) {
s.Select(matchplayer.FieldTeamID, matchplayer.FieldKills, matchplayer.FieldDeaths, matchplayer.FieldAssists, matchplayer.FieldHeadshot,
matchplayer.FieldMvp, matchplayer.FieldScore, matchplayer.FieldMk2, matchplayer.FieldMk3, matchplayer.FieldMk4, matchplayer.FieldMk5,
matchplayer.FieldRankOld, matchplayer.FieldRankNew, matchplayer.FieldDmgTeam, matchplayer.FieldDmgEnemy)
s.Where(sql.EQ(s.C(matchplayer.PlayersColumn), tPlayer.ID))
}).Only(context.Background())
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) {
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.Player(db, id, conf.Steam.APIKey, nil)
if err != nil {
log.Infof("[PPT] player not found: %+v", err)
w.WriteHeader(http.StatusNotFound)
return
}
_, err = utils.IsAuthCodeValid(tPlayer, conf.Steam.APIKey, "", authCode, nil)
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
}
}
err = tPlayer.Update().ClearAuthCode().ClearSharecodeUpdated().Exec(context.Background())
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) {
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.Player(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, 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
}
}
err = tPlayer.Update().SetAuthCode(authCode).Exec(context.Background())
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) {
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) {
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
}
tStats, err := db.MatchPlayer.Query().Where(matchplayer.HasMatchesWith(match.ID(matchId))).All(context.Background())
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 {
tRoundStats, err := stat.QueryRoundStats().All(context.Background())
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) {
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
}
tStats, err := db.MatchPlayer.Query().Where(matchplayer.HasMatchesWith(match.ID(matchId))).All(context.Background())
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"`
Spray []map[string]map[int][][]float32 `json:"spray,omitempty"`
}{
EquipmentMap: map[int]string{},
Stats: []map[string]map[string][][]int{},
Spray: []map[string]map[int][][]float32{},
}
for _, stat := range tStats {
mWs, err := stat.QueryWeaponStats().All(context.Background())
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[playerId]; !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)
mSprays, err := stat.QuerySpray().All(context.Background())
if err != nil {
log.Warningf("[GMW] Unable to get Sprays for player %d: %v", stat.PlayerStats, err)
continue
}
rSprays := map[string]map[int][][]float32{}
for _, spray := range mSprays {
if _, exists := rSprays[playerId]; !exists {
rSprays[playerId] = map[int][][]float32{}
}
bBuf := bytes.NewBuffer(spray.Spray)
dec := gob.NewDecoder(bBuf)
var dSpray [][]float32
err := dec.Decode(&dSpray)
if err != nil {
log.Warningf("[GMW] Unable to decode Sprays for player %d: %v", stat.PlayerStats, err)
continue
}
log.Debugf("%+v", dSpray)
rSprays[playerId][spray.Weapon] = dSpray
}
mResponse.Spray = append(mResponse.Spray, rSprays)
}
err = utils.SendJSON(mResponse, w)
if err != nil {
log.Errorf("[GMW] JSON: %+v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func getMatches(w http.ResponseWriter, r *http.Request) {
t := mux.Vars(r)["time"]
var offsetTime time.Time
if t != "" {
unixOffset, err := strconv.ParseInt(t, 10, 64)
if err != nil {
log.Infof("[GMS] offset not an int: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
offsetTime = time.Unix(unixOffset, 0).UTC()
}
var mResponse []*utils.MatchResponse
var err error
var tMatches []*ent.Match
if !offsetTime.IsZero() {
tMatches, err = db.Match.Query().Where(match.DateLT(offsetTime)).Order(ent.Desc(match.FieldDate)).Limit(20).All(context.Background())
} else {
tMatches, err = db.Match.Query().Order(ent.Desc(match.FieldDate)).Limit(20).All(context.Background())
}
if err != nil || len(tMatches) == 0 {
log.Debug("[GMS] No matches found")
err := utils.SendJSON(mResponse, w)
if err != nil {
log.Errorf("[GMS] Unable to marshal JSON: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}
for _, iMatch := range tMatches {
var v []struct {
Avg float64 `json:"avg"`
MatchID uint64 `json:"match_stats"`
}
err := iMatch.QueryStats().GroupBy(matchplayer.MatchesColumn).Aggregate(ent.Mean(matchplayer.FieldRankOld)).Scan(context.Background(), &v)
if err != nil || len(v) == 0 {
log.Errorf("[GMS] Unable to calc avg rank for match %d: %v", iMatch.ID, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
mResponse = append(mResponse, &utils.MatchResponse{
MatchId: iMatch.ID,
Map: iMatch.Map,
Date: iMatch.Date.Unix(),
Score: [2]int{iMatch.ScoreTeamA, iMatch.ScoreTeamB},
Duration: iMatch.Duration,
MatchResult: iMatch.MatchResult,
MaxRounds: iMatch.MaxRounds,
Parsed: iMatch.DemoParsed,
VAC: iMatch.VacPresent,
GameBan: iMatch.GamebanPresent,
AvgRank: v[0].Avg,
})
}
err = utils.SendJSON(mResponse, w)
if err != nil {
log.Errorf("[GM] JSON: %+v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func getMatch(w http.ResponseWriter, r *http.Request) {
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
}
tMatch, err := db.Match.Query().Where(match.ID(matchId)).Only(context.Background())
if err != nil {
log.Infof("[GM] match %d not found: %v", matchId, err)
w.WriteHeader(http.StatusNotFound)
return
}
var v []struct {
Avg float64 `json:"avg"`
MatchID uint64 `json:"match_stats"`
}
err = tMatch.QueryStats().GroupBy(matchplayer.MatchesColumn).Aggregate(ent.Mean(matchplayer.FieldRankOld)).Scan(context.Background(), &v)
if err != nil || len(v) == 0 {
log.Errorf("[GM] Unable to calc avg rank for match %d: %v", tMatch.ID, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
mResponse := &utils.MatchResponse{
MatchId: tMatch.ID,
ShareCode: tMatch.ShareCode,
Map: tMatch.Map,
Date: tMatch.Date.Unix(),
Score: [2]int{tMatch.ScoreTeamA, tMatch.ScoreTeamB},
Duration: tMatch.Duration,
MatchResult: tMatch.MatchResult,
MaxRounds: tMatch.MaxRounds,
Parsed: tMatch.DemoParsed,
Stats: []*utils.StatsResponse{},
AvgRank: v[0].Avg,
}
if tMatch.Date.After(time.Now().AddDate(0, 0, -1*conf.Csgowtfd.DemosExpire)) {
mResponse.ReplayURL = tMatch.ReplayURL
}
tStats, err := tMatch.QueryStats().WithPlayers().All(context.Background())
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.VacDate.IsZero(),
VACDate: iStats.Edges.Players.VacDate.Unix(),
GameBan: !iStats.Edges.Players.GameBanDate.IsZero(),
GameBanDate: iStats.Edges.Players.GameBanDate.Unix(),
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,
},
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,
},
}
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
/matches GET returns 20 latest matches in DB
/matches/next/<unix> GET returns 20 matches after time unix
*/
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()
}
if conf.Db.Driver == "pgx" {
pdb, err := sql.Open("pgx", conf.Db.ConnectTo)
if err != nil {
log.Fatalf("Failed to open database %s: %v", conf.Db.ConnectTo, err)
}
drv := sql.OpenDB(dialect.Postgres, pdb.DB())
db = ent.NewClient(ent.Driver(drv))
} else {
db, 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)
}
if *sqlDebugFlag {
db = db.Debug()
}
if err := db.Schema.Create(
context.Background(),
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
); err != nil {
log.Fatalf("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: conf.Steam.AuthCode,
Sentry: conf.Steam.Sentry,
LoginKey: conf.Steam.LoginKey,
Db: db,
Worker: conf.Parser.Worker,
ApiKey: conf.Steam.APIKey,
RateLimit: rL,
Cache: rdc,
SprayTimeout: conf.Csgowtfd.SprayTimeout,
})
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}", func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc("/player/{id}", getPlayer).Methods(http.MethodGet)
router.HandleFunc(`/player/{id}/next/{time:\d+}`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/player/{id}/next/{time:\d+}`, getPlayer).Methods(http.MethodGet)
router.HandleFunc(`/player/{id}/meta/{limit:\d+}`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/player/{id}/meta/{limit:\d+}`, getPlayerMeta).Methods(http.MethodGet)
router.HandleFunc("/player/{id}/meta", func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc("/player/{id}/meta", getPlayerMeta).Methods(http.MethodGet)
router.HandleFunc("/player/{id}/track", func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc("/player/{id}/track", postPlayerTrack).Methods(http.MethodPost)
router.HandleFunc("/player/{id}/track", deletePlayerTrack).Methods(http.MethodDelete)
router.HandleFunc("/match/parse/{sharecode}", func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc("/match/parse/{sharecode}", getMatchParse).Methods(http.MethodGet)
router.HandleFunc(`/match/{id:\d{19}}`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/match/{id:\d{19}}`, getMatch).Methods(http.MethodGet)
router.HandleFunc(`/match/{id:\d{19}}/weapons`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/match/{id:\d{19}}/weapons`, getMatchWeapons).Methods(http.MethodGet)
router.HandleFunc(`/match/{id:\d{19}}/rounds`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/match/{id:\d{19}}/rounds`, getMatchRounds).Methods(http.MethodGet)
router.HandleFunc(`/matches`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/matches`, getMatches).Methods(http.MethodGet)
router.HandleFunc(`/matches/next/{time:\d+}`, func(writer http.ResponseWriter, request *http.Request) {}).Methods(http.MethodOptions)
router.HandleFunc(`/matches/next/{time:\d+}`, getMatches).Methods(http.MethodGet)
router.Use(mux.CORSMethodMiddleware(router))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
proxyRouter := handlers.ProxyHeaders(loggedRouter)
cors := handlers.CORS(handlers.AllowedOrigins(conf.Httpd.CORSAllowDomains))
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, cors(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, cors(proxyRouter))
if err != nil {
log.Fatalf("Failure serving on %s:%d: %v", l.Host, l.Port, err)
}
}()
}
}
<-killSignals
for _, s := range sockets {
_ = s.Close()
}
}