1370 lines
38 KiB
Go
1370 lines
38 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/gob"
|
|
"entgo.io/ent/dialect"
|
|
"entgo.io/ent/dialect/sql"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-redis/cache/v8"
|
|
"github.com/go-redis/redis/v8"
|
|
_ "github.com/jackc/pgx/v4/stdlib"
|
|
"github.com/markus-wa/demoinfocs-golang/v3/pkg/demoinfocs/common"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/wercker/journalhook"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/time/rate"
|
|
"gopkg.in/yaml.v3"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"somegit.dev/anonfunc/gositemap"
|
|
"somegit.dev/csgowtf/csgowtfd/csgo"
|
|
"somegit.dev/csgowtf/csgowtfd/ent"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/match"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/matchplayer"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/messages"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/migrate"
|
|
"somegit.dev/csgowtf/csgowtfd/ent/player"
|
|
"somegit.dev/csgowtf/csgowtfd/utils"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
conf = utils.Conf{}
|
|
demoLoader = &csgo.DemoMatchLoader{}
|
|
db *ent.Client
|
|
rdb *redis.Client
|
|
rdc *cache.Cache
|
|
rL *rate.Limiter
|
|
shareCodeRL *rate.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")
|
|
siteMap *gositemap.SiteMap
|
|
)
|
|
|
|
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
|
|
lastUpdated := new(time.Time)
|
|
err = rdc.Get(context.Background(), utils.CachePrefix+"last_updated_profiles", &lastUpdated)
|
|
if err != nil || time.Since(*lastUpdated) >= (time.Hour*24) {
|
|
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)
|
|
}
|
|
|
|
if len(tPlayerNeedSteamUpdate) > 0 {
|
|
log.Infof("[HK] Updating %d profiles from steam (last update %s)", len(tPlayerNeedSteamUpdate), *lastUpdated)
|
|
_, err = utils.PlayerFromSteam(tPlayerNeedSteamUpdate, db, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to update profiles from steam: %v", err)
|
|
goto afterupdate
|
|
}
|
|
}
|
|
err = rdc.Set(&cache.Item{
|
|
Ctx: context.Background(),
|
|
Key: utils.CachePrefix + "last_updated_profiles",
|
|
Value: time.Now().UTC(),
|
|
TTL: time.Hour * 24 * 30,
|
|
SkipLocalCache: true,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("[HK] Failure setting cache: %v", err)
|
|
}
|
|
}
|
|
afterupdate:
|
|
|
|
// 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)), //nolint:gomnd
|
|
)).
|
|
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)), //nolint:gomnd
|
|
)).SetVacPresent(true).Exec(context.Background())
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to set gameban/vac for 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 {
|
|
demo := &csgo.Demo{MatchID: m.ID, ShareCode: m.ShareCode}
|
|
if demoLoader.IsLoading(demo) {
|
|
log.Infof("[HK] Skipping %s: parsing in progress", m.ShareCode)
|
|
continue
|
|
}
|
|
log.Infof("[HK] Try reparsing match %d, played on %s", m.ID, m.Date)
|
|
err := demoLoader.LoadDemo(demo)
|
|
if err != nil {
|
|
log.Warningf("[HK] Failure trying to parse match %d: %v", m.ID, err)
|
|
}
|
|
}
|
|
|
|
// check for inconsistent matches
|
|
tMatchIDs, err := db.Match.Query().IDs(context.Background())
|
|
if err != nil {
|
|
log.Warningf("[HK] Failure trying to get matches: %v", err)
|
|
}
|
|
for _, mid := range tMatchIDs {
|
|
var v []struct {
|
|
ID int `json:"match_stats"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
err = db.MatchPlayer.Query().
|
|
Where(matchplayer.MatchStats(mid)).
|
|
GroupBy(matchplayer.FieldMatchStats).
|
|
Aggregate(ent.Count()).
|
|
Scan(context.Background(), &v)
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to query for matchplayers for match %d: %v", mid, err)
|
|
continue
|
|
}
|
|
|
|
if v[0].Count < 10 {
|
|
log.Warningf("[HK] Found match without all players, try to reload it.")
|
|
tMatch, err := db.Match.Get(context.Background(), mid)
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to get match with id %d: %v", mid, err)
|
|
continue
|
|
}
|
|
err = utils.DeleteMatch(tMatch, db)
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to delete match with id %d: %v", mid, err)
|
|
continue
|
|
}
|
|
|
|
err = demoLoader.LoadDemo(&csgo.Demo{ShareCode: tMatch.ShareCode})
|
|
if err != nil {
|
|
log.Warningf("[HK] Unable to requeue match with id %d: %v", mid, 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, shareCodeRL)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, utils.ErrorAuthcodeUnauthorized):
|
|
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 errors.Is(err, utils.ErrorSharecodeNoMatch):
|
|
log.Warningf("[HK] last shareCode for player %d does not match player", tPlayer.ID)
|
|
continue
|
|
case errors.Is(err, utils.ErrorNoMatch):
|
|
log.Infof("[HK] tracked player %d with no matches found, untracked", 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
|
|
default:
|
|
log.Infof("[HK] Unable to request 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getPlayerMeta(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var limit int
|
|
var err error
|
|
if l := c.Param("limit"); l != "" {
|
|
limit, err = strconv.Atoi(l)
|
|
if err != nil {
|
|
log.Infof("[GPM] limit not an int: %v", err)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
limit = 4
|
|
}
|
|
|
|
if limit > 10 {
|
|
log.Infof("[GPM] limit out of bounds: %d", limit)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tPlayer, err := utils.Player(db, id, conf.Steam.APIKey, nil)
|
|
if err != nil {
|
|
log.Infof("[GPM] Player not found: %+v", err)
|
|
c.Status(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)
|
|
c.Status(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)
|
|
c.Status(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 != "" {
|
|
continue
|
|
}
|
|
|
|
tP, err := utils.Player(db, p.Player.SteamID64, conf.Steam.APIKey, nil)
|
|
if err != nil {
|
|
log.Warningf("[GPM] Failure getting player: %v", err)
|
|
c.Status(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()
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, metaStats)
|
|
}
|
|
|
|
func getPlayer(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var offsetTime time.Time
|
|
if t := c.Param("time"); t != "" {
|
|
unixOffset, err := strconv.ParseInt(t, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GP] offset not an int: %v", err)
|
|
c.Status(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 %s not found (%+v)", id, err)
|
|
c.Status(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)
|
|
c.JSON(http.StatusOK, response)
|
|
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,
|
|
TickRate: iMatch.TickRate,
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func deletePlayerTrack(c *gin.Context) {
|
|
id := c.Param("id")
|
|
authCode := c.PostForm("authcode")
|
|
|
|
if id == "" || authCode == "" || !utils.AuthCodeRegEx.MatchString(authCode) {
|
|
log.Infof("[PPTM] invalid arguments: %+v, %+v", id, authCode)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tPlayer, err := utils.Player(db, id, conf.Steam.APIKey, nil)
|
|
if err != nil {
|
|
log.Infof("[PPT] player not found: %+v", err)
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, err = utils.IsAuthCodeValid(tPlayer, conf.Steam.APIKey, "", authCode, nil)
|
|
if err != nil {
|
|
if errors.Is(err, utils.ErrorAuthcodeUnauthorized) {
|
|
log.Infof("[DPT] authCode provided for player %s is invalid: %v", id, err)
|
|
c.Status(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
log.Infof("[DPT] Temporary Steam-API problem: %v", err)
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
err = tPlayer.Update().ClearAuthCode().ClearSharecodeUpdated().Exec(context.Background())
|
|
if err != nil {
|
|
log.Warningf("[PPT] update player failed: %+v", err)
|
|
c.Status(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
func postPlayerTrack(c *gin.Context) {
|
|
id := c.Param("id")
|
|
authCode := strings.TrimSpace(c.PostForm("authcode"))
|
|
shareCode := strings.TrimSpace(c.PostForm("sharecode"))
|
|
|
|
if id == "" || authCode == "" || !utils.AuthCodeRegEx.MatchString(authCode) {
|
|
log.Infof("[PPT] invalid arguments: %+v, %+v, %+v", id, authCode, shareCode)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tPlayer, err := utils.Player(db, id, conf.Steam.APIKey, rL)
|
|
if err != nil {
|
|
log.Infof("[PPT] player not found: %+v", err)
|
|
c.Status(http.StatusNotFound)
|
|
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)
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err = utils.IsAuthCodeValid(tPlayer, conf.Steam.APIKey, shareCode, authCode, rL)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, utils.ErrorAuthcodeUnauthorized):
|
|
log.Infof("[PPT] authCode provided for player %s is invalid: %v", id, err)
|
|
c.Status(http.StatusUnauthorized)
|
|
return
|
|
case errors.Is(err, utils.ErrorSharecodeNoMatch):
|
|
log.Infof("[PPT] shareCode provided for player %s (%s) is invalid: %v", id, shareCode, err)
|
|
c.Status(http.StatusPreconditionFailed)
|
|
return
|
|
default:
|
|
log.Infof("[PPT] problem with request: %v", err)
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
err = tPlayer.Update().SetAuthCode(authCode).Exec(context.Background())
|
|
if err != nil {
|
|
log.Warningf("[PPT] update player failed: %+v", err)
|
|
c.Status(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)
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
}
|
|
|
|
c.Status(http.StatusAccepted)
|
|
}
|
|
|
|
func getMatchParse(c *gin.Context) {
|
|
shareCode := c.Param("sharecode")
|
|
|
|
if shareCode == "" || !utils.ShareCodeRegEx.MatchString(shareCode) {
|
|
log.Infof("[PPTM] invalid arguments: %s", shareCode)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err := demoLoader.LoadDemo(&csgo.Demo{
|
|
ShareCode: shareCode,
|
|
})
|
|
if err != nil {
|
|
log.Warningf("[PPTM] unable to queue match: %v", err)
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusAccepted)
|
|
}
|
|
|
|
func getMatchRounds(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if id == "" {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchID, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMR] Error parsing matchID %s: %v", id, err)
|
|
c.Status(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)
|
|
c.Status(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}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func getMatchChat(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if id == "" {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tag, weights, err := language.ParseAcceptLanguage(c.GetHeader("Accept-Language"))
|
|
if err != nil {
|
|
log.Warningf("[GMC] Unable to parse Accept-Language %s: %v", c.GetHeader("Accept-Language"), err)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
var lang language.Base
|
|
if len(tag) > 0 {
|
|
lang, _ = tag[0].Base()
|
|
} else {
|
|
lang, _ = language.AmericanEnglish.Base()
|
|
}
|
|
log.Debugf("[GMC] Got header %s, selected %s from %v (w: %v)", c.GetHeader("Accept-Language"), lang, tag, weights)
|
|
|
|
var translate bool
|
|
if trans := c.Query("translate"); trans != "" {
|
|
translate, err = strconv.ParseBool(trans)
|
|
if err != nil {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
matchID, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMC] Error parsing matchID %s: %v", id, err)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := map[string][]*utils.ChatResponse{}
|
|
if translate {
|
|
err = rdc.Get(context.Background(), fmt.Sprintf(utils.MatchChatCacheKey, matchID, lang.String()), &resp)
|
|
if err != nil {
|
|
tStats, err := db.Messages.Query().
|
|
Where(messages.HasMatchPlayerWith(matchplayer.HasMatchesWith(match.ID(matchID)))).
|
|
WithMatchPlayer().
|
|
All(context.Background())
|
|
if err != nil {
|
|
log.Infof("[GMC] match %d not found: %+v", matchID, err)
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
for _, stat := range tStats {
|
|
steamid := strconv.FormatUint(stat.Edges.MatchPlayer.PlayerStats, 10)
|
|
if _, ok := resp[steamid]; !ok {
|
|
resp[steamid] = make([]*utils.ChatResponse, 0)
|
|
}
|
|
|
|
if translate {
|
|
translated, srcLang, err := utils.TranslateWithDeepL(stat.Message, lang.String(),
|
|
conf.DeepL.BaseURL, conf.DeepL.APIKey, conf.DeepL.Timeout)
|
|
if err != nil {
|
|
log.Warningf("[GMC] Unable to translate %s with DeepL: %v", stat.Message, err)
|
|
goto sendNormalResp
|
|
}
|
|
|
|
if srcLang == lang.String() || strings.TrimSpace(translated) == strings.TrimSpace(stat.Message) {
|
|
goto sendNormalResp
|
|
}
|
|
|
|
resp[steamid] = append(resp[steamid], &utils.ChatResponse{
|
|
Message: translated,
|
|
AllChat: stat.AllChat,
|
|
Tick: stat.Tick,
|
|
TranslatedFrom: srcLang,
|
|
TranslatedTo: lang.String(),
|
|
})
|
|
|
|
continue
|
|
}
|
|
sendNormalResp:
|
|
resp[steamid] = append(resp[steamid], &utils.ChatResponse{
|
|
Message: stat.Message,
|
|
AllChat: stat.AllChat,
|
|
Tick: stat.Tick,
|
|
})
|
|
}
|
|
|
|
err = rdc.Set(&cache.Item{
|
|
Ctx: context.Background(),
|
|
Key: fmt.Sprintf(utils.MatchChatCacheKey, matchID, lang.String()),
|
|
Value: resp,
|
|
TTL: time.Hour * 24 * 30,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("[GMC] Failure saving to cache: %v", err)
|
|
c.Status(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
tStats, err := db.Messages.Query().
|
|
Where(messages.HasMatchPlayerWith(matchplayer.HasMatchesWith(match.ID(matchID)))).
|
|
WithMatchPlayer().
|
|
All(context.Background())
|
|
if err != nil {
|
|
log.Infof("[GMC] match %d not found: %+v", matchID, err)
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
for _, stat := range tStats {
|
|
steamid := strconv.FormatUint(stat.Edges.MatchPlayer.PlayerStats, 10)
|
|
if _, ok := resp[steamid]; !ok {
|
|
resp[steamid] = make([]*utils.ChatResponse, 0)
|
|
}
|
|
|
|
resp[steamid] = append(resp[steamid], &utils.ChatResponse{
|
|
Message: stat.Message,
|
|
AllChat: stat.AllChat,
|
|
Tick: stat.Tick,
|
|
})
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func getMatchWeapons(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if id == "" {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchID, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMW] Error parsing matchID %s: %v", id, err)
|
|
c.Status(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)
|
|
c.Status(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)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, mResponse)
|
|
}
|
|
|
|
func getMatches(c *gin.Context) {
|
|
var offsetTime time.Time
|
|
if t := c.Param("time"); t != "" {
|
|
unixOffset, err := strconv.ParseInt(t, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GMS] offset not an int: %v", err)
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
offsetTime = time.Unix(unixOffset, 0).UTC()
|
|
}
|
|
|
|
var mResponse []*utils.MatchResponse //nolint:prealloc
|
|
|
|
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")
|
|
c.JSON(http.StatusOK, mResponse)
|
|
return
|
|
}
|
|
|
|
for _, iMatch := range tMatches {
|
|
var v []struct {
|
|
Avg float64 `json:"avg"`
|
|
MatchID uint64 `json:"match_stats"`
|
|
}
|
|
var avgRank float64
|
|
|
|
err := iMatch.QueryStats().
|
|
Where(matchplayer.RankOldNEQ(0)).
|
|
GroupBy(matchplayer.MatchesColumn).
|
|
Aggregate(ent.Mean(matchplayer.FieldRankOld)).
|
|
Scan(context.Background(), &v)
|
|
if err != nil || len(v) == 0 {
|
|
log.Debugf("[GMS] Unable to calc avg rank for match %d: %v", iMatch.ID, err)
|
|
avgRank = 0.0
|
|
} else {
|
|
avgRank = v[0].Avg
|
|
}
|
|
|
|
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: avgRank,
|
|
TickRate: iMatch.TickRate,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, mResponse)
|
|
}
|
|
|
|
func getMatch(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if id == "" {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchID, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil {
|
|
log.Infof("[GM] Unable to parse matchID %s: %v", id, err)
|
|
c.Status(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)
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var v []struct {
|
|
Avg float64 `json:"avg"`
|
|
MatchID uint64 `json:"match_stats"`
|
|
}
|
|
|
|
var avgRank float64
|
|
err = tMatch.QueryStats().
|
|
Where(matchplayer.RankOldNEQ(0)).
|
|
GroupBy(matchplayer.MatchesColumn).
|
|
Aggregate(ent.Mean(matchplayer.FieldRankOld)).
|
|
Scan(context.Background(), &v)
|
|
if err != nil || len(v) == 0 {
|
|
log.Debugf("[GM] Unable to calc avg rank for match %d: %v", tMatch.ID, err)
|
|
avgRank = 0
|
|
} else {
|
|
avgRank = v[0].Avg
|
|
}
|
|
|
|
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: avgRank,
|
|
TickRate: tMatch.TickRate,
|
|
}
|
|
|
|
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)
|
|
c.Status(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
|
|
|
|
c.JSON(http.StatusOK, mResponse)
|
|
}
|
|
|
|
func getSiteMapIndex(c *gin.Context) {
|
|
res, err := siteMap.SiteMapIndex()
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
var compBytes bytes.Buffer
|
|
compWriter := gzip.NewWriter(&compBytes)
|
|
if _, err = compWriter.Write(res); err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if err = compWriter.Close(); err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Encoding", "gzip")
|
|
if c.Request.Method == http.MethodHead {
|
|
c.Header("Content-Length", strconv.Itoa(compBytes.Len()))
|
|
c.Header("Content-Type", "application/xml")
|
|
c.Status(http.StatusOK)
|
|
} else {
|
|
c.Data(http.StatusOK, "application/xml", compBytes.Bytes())
|
|
}
|
|
}
|
|
|
|
func getSiteMap(c *gin.Context) {
|
|
if c.Param("id") == "" {
|
|
_ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no index specified"))
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
res, err := siteMap.SiteMap(id)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if res == nil {
|
|
_ = c.AbortWithError(http.StatusNotFound, fmt.Errorf("sitemap does not exist"))
|
|
return
|
|
}
|
|
|
|
var compBytes bytes.Buffer
|
|
compWriter := gzip.NewWriter(&compBytes)
|
|
if _, err = compWriter.Write(res); err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if err = compWriter.Close(); err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Encoding", "gzip")
|
|
if c.Request.Method == http.MethodHead {
|
|
c.Header("Content-Length", strconv.Itoa(compBytes.Len()))
|
|
c.Header("Content-Type", "application/xml")
|
|
c.Status(http.StatusOK)
|
|
} else {
|
|
c.Data(http.StatusOK, "application/xml", compBytes.Bytes())
|
|
}
|
|
}
|
|
|
|
/*
|
|
/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)
|
|
|
|
reloadSignals := make(chan os.Signal, 1)
|
|
signal.Notify(reloadSignals, syscall.SIGUSR1)
|
|
|
|
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.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 = rate.NewLimiter(rate.Limit(conf.Steam.RatePerSecond), 1000)
|
|
shareCodeRL = rate.NewLimiter(rate.Limit(0.2), 1) //nolint:gomnd
|
|
|
|
// 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,
|
|
RetryTimeout: conf.Steam.MaxRetryWait,
|
|
})
|
|
if err != nil {
|
|
log.Panicf("Error setting up DemoLoader: %v", err)
|
|
}
|
|
|
|
// start housekeeper
|
|
go housekeeping()
|
|
|
|
// populate sitemap
|
|
siteMap = &gositemap.SiteMap{BaseURL: "csgow.tf", MaxURLPerSiteMap: 9000}
|
|
freq := gositemap.Hourly
|
|
siteMap.AddURL("/matches", nil, &freq, nil)
|
|
|
|
players, err := db.Player.Query().IDs(context.Background())
|
|
if err != nil {
|
|
log.Panicf("error setting up SiteMap: %v", err)
|
|
}
|
|
|
|
for _, tPlayer := range players {
|
|
freq := gositemap.Daily
|
|
siteMap.AddURL(fmt.Sprintf("/player/%d", tPlayer), nil, &freq, nil)
|
|
}
|
|
|
|
matches, err := db.Match.Query().IDs(context.Background())
|
|
if err != nil {
|
|
log.Panicf("error setting up SiteMap: %v", err)
|
|
}
|
|
|
|
for _, tMatch := range matches {
|
|
freq := gositemap.Weekly
|
|
siteMap.AddURL(fmt.Sprintf("/match/%d", tMatch), nil, &freq, nil)
|
|
}
|
|
|
|
// routes
|
|
r := gin.New()
|
|
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
|
return fmt.Sprintf("%s - - \"%s %s %s\" %d %d %q %q %s %s\n",
|
|
utils.RealIP(¶m.Request.Header, param.Request.RemoteAddr),
|
|
param.Method,
|
|
param.Path,
|
|
param.Request.Proto,
|
|
param.StatusCode,
|
|
param.BodySize,
|
|
param.Request.Header.Get("Referer"),
|
|
param.Request.UserAgent(),
|
|
param.Latency,
|
|
strings.Trim(param.ErrorMessage, "\n"),
|
|
)
|
|
}), gin.Recovery())
|
|
config := cors.DefaultConfig()
|
|
config.AllowOrigins = conf.Httpd.CORSAllowDomains
|
|
|
|
r.Use(cors.New(config))
|
|
|
|
r.GET("/player/:id", getPlayer)
|
|
r.GET("/player/:id/next/:time", getPlayer)
|
|
r.GET("/player/:id/meta/:limit", getPlayerMeta)
|
|
r.GET("/player/:id/meta", getPlayerMeta)
|
|
r.POST("/player/:id/track", postPlayerTrack)
|
|
r.DELETE("/player/:id/track", deletePlayerTrack)
|
|
r.GET("/match/parse/:sharecode", getMatchParse)
|
|
r.GET("/match/:id", getMatch)
|
|
r.GET("/match/:id/weapons", getMatchWeapons)
|
|
r.GET("/match/:id/rounds", getMatchRounds)
|
|
r.GET("/match/:id/chat", getMatchChat)
|
|
r.GET("/matches", getMatches)
|
|
r.GET("/matches/next/:time", getMatches)
|
|
r.GET("/sitemap.xml", getSiteMapIndex)
|
|
r.HEAD("/sitemap.xml", getSiteMapIndex)
|
|
r.GET("/sitemap/index", getSiteMapIndex)
|
|
r.HEAD("/sitemap/index", getSiteMapIndex)
|
|
r.GET("/sitemap/:id", getSiteMap)
|
|
r.HEAD("/sitemap/:id", getSiteMap)
|
|
|
|
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)
|
|
log.Infof("Listening on %s", l.Socket)
|
|
if err != nil {
|
|
log.Panicf("Failure listing on socket %s: %v", l.Socket, err)
|
|
}
|
|
sockets = append(sockets, sL)
|
|
go func() {
|
|
srv := &http.Server{
|
|
ReadTimeout: time.Duration(conf.Csgowtfd.Timeout.Read) * time.Second,
|
|
WriteTimeout: time.Duration(conf.Csgowtfd.Timeout.Write) * time.Second,
|
|
IdleTimeout: time.Duration(conf.Csgowtfd.Timeout.Idle) * time.Second,
|
|
Handler: r,
|
|
}
|
|
_ = srv.Serve(sL)
|
|
}()
|
|
} else {
|
|
log.Infof("Listening on %s:%d", l.Host, l.Port)
|
|
tL, err := net.Listen("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port))
|
|
if err != nil {
|
|
log.Panicf("Failure listing on %s:%d: %v", l.Host, l.Port, err)
|
|
}
|
|
go func(l struct {
|
|
Socket string
|
|
Host string
|
|
Port int
|
|
}) {
|
|
srv := &http.Server{
|
|
ReadTimeout: time.Duration(conf.Csgowtfd.Timeout.Read) * time.Second,
|
|
WriteTimeout: time.Duration(conf.Csgowtfd.Timeout.Write) * time.Second,
|
|
IdleTimeout: time.Duration(conf.Csgowtfd.Timeout.Idle) * time.Second,
|
|
Handler: r,
|
|
}
|
|
err = srv.Serve(tL)
|
|
if err != nil {
|
|
log.Fatalf("Failure serving on %s:%d: %v", l.Host, l.Port, err)
|
|
}
|
|
}(l)
|
|
}
|
|
}
|
|
|
|
killLoop:
|
|
for {
|
|
select {
|
|
case <-killSignals:
|
|
break killLoop
|
|
case <-reloadSignals:
|
|
confStr, err := os.ReadFile(*configFlag)
|
|
if err != nil {
|
|
log.Panicf("Unable to open config: %v", err)
|
|
}
|
|
|
|
err = yaml.Unmarshal(confStr, &conf)
|
|
if err != nil {
|
|
log.Panicf("Unable to parse config: %v", err)
|
|
}
|
|
|
|
lvl, err := log.ParseLevel(conf.Logging.Level)
|
|
if err != nil {
|
|
log.Panicf("Failure setting logging level: %v", err)
|
|
}
|
|
log.SetLevel(lvl)
|
|
}
|
|
}
|
|
|
|
for _, s := range sockets {
|
|
_ = s.Close()
|
|
}
|
|
}
|