package utils import ( "context" "csgowtfd/csgo" "csgowtfd/ent" "csgowtfd/ent/match" "csgowtfd/ent/player" "encoding/json" "encoding/xml" "fmt" "github.com/Philipp15b/go-steamapi" log "github.com/sirupsen/logrus" "go.uber.org/ratelimit" "io" "io/ioutil" "net/http" "regexp" "strconv" "strings" "sync" "time" ) type Conf struct { Logging struct { Level string } Parser struct { Worker int } Steam struct { Username string APIKey string `yaml:"api_key"` RatePerSecond int `yaml:"rate_per_sec"` } Httpd struct { CORSAllowDomains string `yaml:"cors_allow_domains"` } } type DBWithLock struct { Client *ent.Client Lock *sync.RWMutex } type CommunityXML struct { SteamID64 uint64 `xml:"steamID64"` AvatarURL string `xml:"avatarFull"` VacBanned bool `xml:"vacBanned"` ProfileName string `xml:"steamID"` Error string `xml:"error"` VanityURL string `xml:"customURL"` } type shareCodeResponse struct { Result struct { Code string `json:"nextcode"` } `json:"result"` } const ( steamID64Entry = "https://steamcommunity.com/profiles/%d?xml=1" steamVanityURLEntry = "https://steamcommunity.com/id/%s?xml=1" shareCodeURLEntry = "https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=%s&steamid=%d&steamidkey=%s&knowncode=%s" ) var ( SteamId64RegEx = regexp.MustCompile(`^\d{17}$`) ShareCodeRegEx = regexp.MustCompile(`^CSGO(?:-?[ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789]{5}){5}$`) AuthCodeRegEx = regexp.MustCompile(`^[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{5}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}$`) ) func SendJSON(data interface{}, w http.ResponseWriter) error { playerJson, err := json.Marshal(data) if err != nil { return err } w.WriteHeader(http.StatusOK) _, err = w.Write(playerJson) if err != nil { return err } return nil } func IsAuthCodeValid(player *ent.Player, lock *sync.RWMutex, apiKey string, shareCode string, authCode string, rl ratelimit.Limiter) (bool, error) { var tMatch *ent.Match var err error if shareCode == "" { lock.RLock() tMatch, err = player.QueryMatches().Order(ent.Desc(match.FieldDate)).First(context.Background()) lock.RUnlock() if err != nil { return false, err } _, err := getNextShareCode(tMatch.ShareCode, apiKey, authCode, player.Steamid, rl) if err != nil { return false, err } return true, nil } else { _, err := getNextShareCode(shareCode, apiKey, authCode, player.Steamid, rl) if err != nil { return false, err } return true, nil } } func GetNewShareCodesForPlayer(player *ent.Player, lock *sync.RWMutex, apiKey string, rl ratelimit.Limiter) ([]string, error) { lock.RLock() tMatch, err := player.QueryMatches().Order(ent.Desc(match.FieldDate)).First(context.Background()) lock.RUnlock() if err != nil { return nil, err } var rCodes []string newShareCode, err := getNextShareCode(tMatch.ShareCode, apiKey, player.AuthCode, player.Steamid, rl) if err != nil { return nil, err } for newShareCode != "n/a" { rCodes = append(rCodes, newShareCode) newShareCode, err = getNextShareCode(rCodes[len(rCodes)-1], apiKey, player.AuthCode, player.Steamid, rl) if err != nil { return nil, err } time.Sleep(time.Second) } lock.Lock() err = player.Update().SetSharecodeUpdated(time.Now().UTC()).Exec(context.Background()) lock.Unlock() if err != nil { return nil, err } return rCodes, nil } func getNextShareCode(lastCode string, apiKey string, authCode string, steamId uint64, rl ratelimit.Limiter) (string, error) { if lastCode == "" || apiKey == "" || authCode == "" || steamId == 0 { return "", fmt.Errorf("invalid arguments") } rl.Take() log.Debugf("[SC] STEAMPI with %s", fmt.Sprintf(shareCodeURLEntry, apiKey, steamId, authCode, lastCode)) r, err := http.Get(fmt.Sprintf(shareCodeURLEntry, apiKey, steamId, authCode, lastCode)) if err != nil { return "", err } if r.StatusCode == 202 { return "n/a", nil } else if r.StatusCode != 200 { return "", fmt.Errorf("bad response from steam api (HTTP %d)", r.StatusCode) } defer r.Body.Close() bJson, err := ioutil.ReadAll(r.Body) if err != nil { return "", err } rJson := new(shareCodeResponse) err = json.Unmarshal(bJson, rJson) if err != nil { return "", err } return rJson.Result.Code, nil } func GetPlayer(db *DBWithLock, id interface{}, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) { switch e := id.(type) { case uint64: return GetPlayerFromSteamID64(db, e, apiKey, rl) case string: if SteamId64RegEx.MatchString(e) { steamID64, err := strconv.ParseUint(e, 10, 64) if err != nil { return nil, err } return GetPlayerFromSteamID64(db, steamID64, apiKey, rl) } return GetPlayerFromVanityURL(db, e) default: return nil, fmt.Errorf("invalid arguments") } } func GetPlayerFromVanityURL(db *DBWithLock, id string) (*ent.Player, error) { if id == "" { return nil, fmt.Errorf("invalid arguments") } db.Lock.RLock() tPlayer, err := db.Client.Player.Query().Where(player.VanityURL(strings.ToLower(id))).Only(context.Background()) db.Lock.RUnlock() if err == nil { return tPlayer, nil } else { profile, err := SteamProfile2XML(id, 0) if err != nil { return nil, err } if profile.Error != "" { return nil, fmt.Errorf("profile not found") } db.Lock.Lock() nPlayer, err := db.Client.Player.Create().SetSteamid(profile.SteamID64).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetName(profile.ProfileName).Save(context.Background()) db.Lock.Unlock() if err != nil { return nil, err } return nPlayer, nil } } func GetPlayerFromSteamID64(db *DBWithLock, steamID uint64, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) { db.Lock.RLock() tPlayer, err := db.Client.Player.Query().Where(player.Steamid(steamID)).Only(context.Background()) db.Lock.RUnlock() if err == nil { return tPlayer, nil } else { db.Lock.Lock() nPlayer, err := db.Client.Player.Create().SetSteamid(steamID).Save(context.Background()) db.Lock.Unlock() if err != nil { return nil, err } nPlayer, err = UpdatePlayerFromSteam(nPlayer, apiKey, db.Lock, rl) if err != nil { return nil, err } return nPlayer, nil } } func GCInfoParser(channel chan *csgo.Demo, dl *csgo.DemoMatchLoader, dp *csgo.DemoParser, db *DBWithLock, apiKey string, rl ratelimit.Limiter) { for { select { case demo := <-channel: if !dl.GCReady { time.Sleep(5 * time.Second) channel <- demo } matchId, _, _, err := csgo.DecodeSharecode(demo.ShareCode) Check(err) if matchId == 0 { log.Warningf("Can't parse match with sharecode %s", demo.ShareCode) continue } db.Lock.RLock() iMatch, err := db.Client.Match.Query().Where(match.MatchID(matchId)).Only(context.Background()) db.Lock.RUnlock() if err != nil { switch e := err.(type) { case *ent.NotFoundError: break default: Check(e) } } else { if iMatch.DemoParsed == false && !iMatch.DemoExpired { log.Infof("Match %d is loaded, but not parsed. Try parsing.", demo.MatchId) demo.MatchId = matchId demo.Url = iMatch.ReplayURL dp.ParseDemo(demo) continue } log.Debugf("Skipped match %d: already parsed", matchId) continue } matchDetails, err := dl.GetMatchDetails(demo.ShareCode) Check(err) matchZero := matchDetails.GetMatches()[0] lastRound := matchZero.GetRoundstatsall()[len(matchZero.Roundstatsall)-1] var players []*ent.Player for _, accountId := range lastRound.GetReservation().GetAccountIds() { tPlayer, err := GetPlayer(db, csgo.AccountId2SteamId(accountId), apiKey, rl) Check(err) players = append(players, tPlayer) } demo.Url = lastRound.GetMap() demo.MatchId = matchZero.GetMatchid() db.Lock.Lock() tMatch, err := db.Client.Match.Create(). SetMatchID(matchZero.GetMatchid()). AddPlayers(players...). SetDate(time.Unix(int64(matchZero.GetMatchtime()), 0).UTC()). SetMaxRounds(int(lastRound.GetMaxRounds())). SetDuration(int(lastRound.GetMatchDuration())). SetShareCode(demo.ShareCode). SetReplayURL(lastRound.GetMap()). SetScoreTeamA(int(lastRound.GetTeamScores()[0])). SetScoreTeamB(int(lastRound.GetTeamScores()[1])). SetMatchResult(int(lastRound.GetMatchResult())). Save(context.Background()) db.Lock.Unlock() Check(err) for i, mPlayer := range players { var teamId int if i > 4 { teamId = 2 } else { teamId = 1 } db.Lock.Lock() err := db.Client.Stats.Create(). SetMatches(tMatch). SetPlayers(mPlayer). SetTeamID(teamId). SetKills(int(lastRound.GetKills()[i])). SetDeaths(int(lastRound.GetDeaths()[i])). SetAssists(int(lastRound.GetAssists()[i])). SetMvp(int(lastRound.GetMvps()[i])). SetScore(int(lastRound.GetScores()[i])). SetHeadshot(int(lastRound.GetEnemyHeadshots()[i])). Exec(context.Background()) db.Lock.Unlock() Check(err) } dp.ParseDemo(demo) } } } func SteamProfile2XML(id string, steamID64 uint64) (*CommunityXML, error) { var r *http.Response var err error if steamID64 != 0 { r, err = http.Get(fmt.Sprintf(steamID64Entry, steamID64)) } else { r, err = http.Get(fmt.Sprintf(steamVanityURLEntry, id)) } Check(err) defer func(Body io.ReadCloser) { err := Body.Close() Check(err) }(r.Body) body, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } cXML := &CommunityXML{} err = xml.Unmarshal(body, cXML) if err != nil { return nil, err } return cXML, nil } func UpdatePlayerFromSteam(player *ent.Player, apiKey string, lock *sync.RWMutex, rl ratelimit.Limiter) (*ent.Player, error) { profile, err := SteamProfile2XML("", player.Steamid) if err != nil { return nil, err } if profile.Error != "" { return nil, fmt.Errorf("profile not found") } lock.Lock() tPlayer, err := player.Update().SetName(profile.ProfileName).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetSteamUpdated(time.Now().UTC()).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVanityURLReal(profile.VanityURL).SetSteamUpdated(time.Now().UTC()).Save(context.Background()) lock.Unlock() if err != nil { return nil, err } rl.Take() bans, err := steamapi.GetPlayerBans([]uint64{profile.SteamID64}, apiKey) if err != nil { return nil, err } if len(bans) > 0 && bans[0].NumberOfVACBans > 0 { banDate := time.Now().UTC().AddDate(0, 0, -1*int(bans[0].DaysSinceLastBan)) lock.Lock() err := tPlayer.Update().SetVacCount(int(bans[0].NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background()) lock.Unlock() if err != nil { return nil, err } } return tPlayer, nil } func Check(e error) { if e != nil { panic(e) } }