import from unified repo
This commit is contained in:
403
main.go
Normal file
403
main.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"csgowtfd/csgo"
|
||||
"csgowtfd/ent"
|
||||
"csgowtfd/ent/match"
|
||||
"csgowtfd/ent/migrate"
|
||||
"csgowtfd/ent/player"
|
||||
"csgowtfd/ent/stats"
|
||||
"csgowtfd/utils"
|
||||
"flag"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.uber.org/ratelimit"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
conf = utils.Conf{}
|
||||
demoLoader = &csgo.DemoMatchLoader{}
|
||||
router *mux.Router
|
||||
db *utils.DBWithLock
|
||||
sendGC chan *csgo.Demo
|
||||
demoParser = &csgo.DemoParser{}
|
||||
firstHK = true
|
||||
rL ratelimit.Limiter
|
||||
)
|
||||
|
||||
type PlayerResponse struct {
|
||||
SteamID64 uint64 `json:"steamid64,string"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
VAC bool `json:"vac"`
|
||||
Tracked bool `json:"tracked"`
|
||||
VanityURL string `json:"vanity_url,omitempty"`
|
||||
Matches []*MatchResponse `json:"matches,omitempty"`
|
||||
}
|
||||
|
||||
type MatchResponse struct {
|
||||
MatchId uint64 `json:"match_id,string"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Map string `json:"map"`
|
||||
Date time.Time `json:"date"`
|
||||
Score [2]int `json:"score"`
|
||||
Duration int `json:"duration"`
|
||||
MatchResult int `json:"match_result"`
|
||||
Rounds int `json:"rounds"`
|
||||
Parsed bool `json:"parsed"`
|
||||
Stats []*StatsResponse `json:"stats"`
|
||||
}
|
||||
|
||||
type StatsResponse struct {
|
||||
TeamID int `json:"team_id"`
|
||||
Kills int `json:"kills"`
|
||||
Deaths int `json:"deaths"`
|
||||
Assists int `json:"assists"`
|
||||
Headshot int `json:"headshot"`
|
||||
MVP int `json:"mvp"`
|
||||
Score int `json:"score"`
|
||||
Player PlayerResponse `json:"player"`
|
||||
Extended interface{} `json:"extended,omitempty"`
|
||||
}
|
||||
|
||||
func housekeeping() {
|
||||
for {
|
||||
if !firstHK {
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
firstHK = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for _, tPlayer := range tPlayerNeedSteamUpdate {
|
||||
_, err = utils.UpdatePlayerFromSteam(tPlayer, conf.Steam.APIKey, db.Lock, rL)
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Errorf("[HK] Error while request sharecodes: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, code := range shareCodes {
|
||||
sendGC <- &csgo.Demo{
|
||||
ShareCode: code,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPlayer(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
tPlayer, err := utils.GetPlayer(db, id, conf.Steam.APIKey, rL)
|
||||
if err != nil {
|
||||
log.Warningf("[GP] Player not found: %+v", err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := PlayerResponse{
|
||||
SteamID64: tPlayer.Steamid,
|
||||
Name: tPlayer.Name,
|
||||
Avatar: tPlayer.AvatarURL,
|
||||
VAC: tPlayer.Vac,
|
||||
VanityURL: tPlayer.VanityURLReal,
|
||||
Tracked: tPlayer.AuthCode != "",
|
||||
Matches: []*MatchResponse{},
|
||||
}
|
||||
|
||||
db.Lock.RLock()
|
||||
tMatches, err := tPlayer.QueryMatches().Order(ent.Desc(match.FieldDate)).Limit(20).All(context.Background())
|
||||
db.Lock.RUnlock()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
for _, iMatch := range tMatches {
|
||||
mResponse := &MatchResponse{
|
||||
MatchId: iMatch.MatchID,
|
||||
ShareCode: iMatch.ShareCode,
|
||||
Map: iMatch.Map,
|
||||
Date: iMatch.Date,
|
||||
Score: [2]int{iMatch.ScoreTeamA, iMatch.ScoreTeamB},
|
||||
Duration: iMatch.Duration,
|
||||
MatchResult: iMatch.MatchResult,
|
||||
Rounds: iMatch.MaxRounds,
|
||||
Parsed: iMatch.DemoParsed,
|
||||
Stats: []*StatsResponse{},
|
||||
}
|
||||
|
||||
db.Lock.RLock()
|
||||
tStats, err := iMatch.QueryStats().Where(stats.HasPlayersWith(player.Steamid(tPlayer.Steamid))).WithPlayers().All(context.Background())
|
||||
db.Lock.RUnlock()
|
||||
if err != nil {
|
||||
response.Matches = append(response.Matches, mResponse)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, iStats := range tStats {
|
||||
sResponse := &StatsResponse{
|
||||
TeamID: iStats.TeamID,
|
||||
Kills: iStats.Kills,
|
||||
Deaths: iStats.Deaths,
|
||||
Assists: iStats.Assists,
|
||||
Headshot: iStats.Headshot,
|
||||
MVP: iStats.Mvp,
|
||||
Score: iStats.Score,
|
||||
Extended: iStats.Extended,
|
||||
}
|
||||
|
||||
mResponse.Stats = append(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 postPlayerTrackMe(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
log.Errorf("[postPlayerTrackMe] %+v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.Form.Get("id")
|
||||
authCode := r.Form.Get("authcode")
|
||||
shareCode := r.Form.Get("sharecode")
|
||||
|
||||
if id == "" || authCode == "" || !utils.AuthCodeRegEx.MatchString(authCode) {
|
||||
log.Warningf("[PPTM] 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.Warningf("[PPTM] 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 {
|
||||
log.Warningf("[PPTM] authCode provided for player %s is invalid: %v", id, err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
db.Lock.Lock()
|
||||
err = tPlayer.Update().SetAuthCode(authCode).Exec(context.Background())
|
||||
db.Lock.Unlock()
|
||||
if err != nil {
|
||||
log.Warningf("[PPTM] update player failed: %+v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if shareCode != "" && utils.ShareCodeRegEx.MatchString(shareCode) {
|
||||
sendGC <- &csgo.Demo{ShareCode: shareCode}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func getMatchParse(w http.ResponseWriter, r *http.Request) {
|
||||
shareCode := mux.Vars(r)["sharecode"]
|
||||
|
||||
if shareCode == "" || !utils.ShareCodeRegEx.MatchString(shareCode) {
|
||||
log.Warningf("[PPTM] invalid arguments")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sendGC <- &csgo.Demo{
|
||||
ShareCode: shareCode,
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
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.Warningf("[GM] Error parsing matchID %s: %v", id, err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db.Lock.RLock()
|
||||
tMatch, err := db.Client.Match.Query().Where(match.MatchID(matchId)).Only(context.Background())
|
||||
db.Lock.RUnlock()
|
||||
if err != nil {
|
||||
log.Warningf("[GM] match %d not found: %+v", matchId, err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
mResponse := &MatchResponse{
|
||||
MatchId: tMatch.MatchID,
|
||||
ShareCode: tMatch.ShareCode,
|
||||
Map: tMatch.Map,
|
||||
Date: tMatch.Date,
|
||||
Score: [2]int{tMatch.ScoreTeamA, tMatch.ScoreTeamB},
|
||||
Duration: tMatch.Duration,
|
||||
MatchResult: tMatch.MatchResult,
|
||||
Rounds: tMatch.MaxRounds,
|
||||
Parsed: tMatch.DemoParsed,
|
||||
Stats: []*StatsResponse{},
|
||||
}
|
||||
|
||||
db.Lock.RLock()
|
||||
tStats, err := tMatch.QueryStats().WithPlayers().All(context.Background())
|
||||
db.Lock.RUnlock()
|
||||
if err != nil {
|
||||
log.Errorf("[GM] can't find stats for match %d: %v", tMatch.MatchID, err)
|
||||
}
|
||||
|
||||
for _, iStats := range tStats {
|
||||
sResponse := &StatsResponse{
|
||||
Player: PlayerResponse{
|
||||
SteamID64: iStats.Edges.Players.Steamid,
|
||||
Name: iStats.Edges.Players.Name,
|
||||
Avatar: iStats.Edges.Players.AvatarURL,
|
||||
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,
|
||||
Extended: iStats.Extended,
|
||||
}
|
||||
|
||||
mResponse.Stats = append(mResponse.Stats, sResponse)
|
||||
}
|
||||
|
||||
err = utils.SendJSON(mResponse, w)
|
||||
if err != nil {
|
||||
log.Errorf("[GM] JSON: %+v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/player/<id> GET player internal or if not found: steamAPI data + overall stats
|
||||
/player/trackme POST id, authcode, [sharecode]
|
||||
/match/<id> GET CSGO-GC response + internal data if parsed <- may be big (ALL RELEVANT DATA)
|
||||
/match/parse/<sharecode> GET parses sharecode provided
|
||||
*/
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
confStr, err := os.ReadFile("config.yaml")
|
||||
utils.Check(err)
|
||||
|
||||
err = yaml.Unmarshal(confStr, &conf)
|
||||
utils.Check(err)
|
||||
|
||||
lvl, err := log.ParseLevel(conf.Logging.Level)
|
||||
utils.Check(err)
|
||||
log.SetLevel(lvl)
|
||||
|
||||
db = &utils.DBWithLock{
|
||||
Lock: new(sync.RWMutex),
|
||||
}
|
||||
|
||||
db.Client, err = ent.Open("sqlite3", "file:opencsgo.db?_fk=1&cache=shared")
|
||||
if err != nil {
|
||||
log.Panicf("Failed to open database %s: %v", "opencsgo.db", err)
|
||||
}
|
||||
defer func(dbSQLite *ent.Client) {
|
||||
utils.Check(dbSQLite.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)
|
||||
}
|
||||
|
||||
rL = ratelimit.New(conf.Steam.RatePerSecond)
|
||||
// setup GC
|
||||
err = demoLoader.Setup(conf.Steam.Username)
|
||||
if err != nil {
|
||||
log.Fatalf("Unbale to setup DemoLoader: %v", err)
|
||||
}
|
||||
|
||||
log.Info("Waiting for GC to be ready")
|
||||
for demoLoader.GCReady != true {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
log.Info("GC ready, starting HTTP server")
|
||||
|
||||
sendGC = make(chan *csgo.Demo, 100)
|
||||
utils.Check(demoParser.Setup(db.Client, db.Lock))
|
||||
go utils.GCInfoParser(sendGC, demoLoader, demoParser, db, conf.Steam.APIKey, rL)
|
||||
go housekeeping()
|
||||
|
||||
router = mux.NewRouter().StrictSlash(true)
|
||||
router.HandleFunc("/player/{id}", getPlayer).Methods("GET")
|
||||
router.HandleFunc("/player/trackme", postPlayerTrackMe).Methods("POST")
|
||||
router.HandleFunc("/match/parse/{sharecode}", getMatchParse).Methods("GET")
|
||||
router.HandleFunc("/match/{id:[0-9]{19}}", getMatch).Methods("GET")
|
||||
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
|
||||
utils.Check(http.ListenAndServe(":8000", loggedRouter))
|
||||
}
|
Reference in New Issue
Block a user