2022-07-24 16:19:27 +00:00
|
|
|
package tw
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/csv"
|
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2023-02-11 07:36:56 +00:00
|
|
|
urlpkg "net/url"
|
2022-08-16 03:56:12 +00:00
|
|
|
"sort"
|
2022-07-24 16:19:27 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
2022-09-12 05:34:46 +00:00
|
|
|
"github.com/elliotchance/phpserialize"
|
2022-07-24 16:19:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2022-08-21 05:19:17 +00:00
|
|
|
fieldsPerRecordPlayer = 6
|
|
|
|
fieldsPerRecordTribe = 8
|
|
|
|
fieldsPerRecordVillage = 7
|
|
|
|
fieldsPerRecordOD = 3
|
|
|
|
fieldsPerRecordEnnoblement = 7
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
endpointPlayers = "/map/player.txt"
|
|
|
|
endpointTribes = "/map/ally.txt"
|
|
|
|
endpointVillages = "/map/village.txt"
|
|
|
|
endpointPlayersODA = "/map/kill_att.txt"
|
|
|
|
endpointPlayersODD = "/map/kill_def.txt"
|
|
|
|
endpointPlayersODS = "/map/kill_sup.txt"
|
|
|
|
endpointPlayersOD = "/map/kill_all.txt"
|
|
|
|
endpointTribesODA = "/map/kill_att_tribe.txt"
|
|
|
|
endpointTribesODD = "/map/kill_def_tribe.txt"
|
|
|
|
endpointTribesOD = "/map/kill_all_tribe.txt"
|
|
|
|
endpointMapEnnoblements = "/map/conquer_extended.txt"
|
|
|
|
endpointInterface = "/interface.php"
|
|
|
|
endpointGetServers = "/backend/get_servers.php"
|
|
|
|
endpointGame = "/game.php"
|
|
|
|
|
|
|
|
queryConfig = "func=get_config"
|
|
|
|
queryUnitInfo = "func=get_unit_info"
|
|
|
|
queryBuildingInfo = "func=get_building_info"
|
|
|
|
queryInterfaceEnnoblements = "func=get_conquer_extended&since=%d"
|
|
|
|
queryPlayerProfile = "screen=info_player&id=%d"
|
|
|
|
queryTribeProfile = "screen=info_ally&id=%d"
|
|
|
|
queryVillageProfile = "screen=info_village&id=%d"
|
2022-07-24 16:19:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrInvalidServerKey = errors.New("invalid server key")
|
|
|
|
ErrInvalidServerURL = errors.New("invalid server URL")
|
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
2023-02-23 06:45:52 +00:00
|
|
|
client *http.Client
|
|
|
|
userAgent string
|
|
|
|
ennoblementsUseInterfaceFunc func(since time.Time) bool
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient(opts ...ClientOption) *Client {
|
2023-02-23 06:45:52 +00:00
|
|
|
cfg := newClientConfig(opts...)
|
|
|
|
return &Client{
|
|
|
|
client: cfg.client,
|
|
|
|
userAgent: cfg.userAgent,
|
|
|
|
ennoblementsUseInterfaceFunc: cfg.ennoblementsUseInterfaceFunc,
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
type Server struct {
|
|
|
|
Key string
|
|
|
|
URL string
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetOpenServers(ctx context.Context, baseURLRaw string) ([]Server, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-02-16 06:37:48 +00:00
|
|
|
url := buildURL(baseURL, endpointGetServers)
|
|
|
|
resp, err := c.get(ctx, url)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("c.get: %w", err)
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
|
|
|
|
2022-08-16 05:20:44 +00:00
|
|
|
b, err := io.ReadAll(resp.Body)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("couldn't read response body: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-08-16 05:20:44 +00:00
|
|
|
m, err := phpserialize.UnmarshalAssociativeArray(b)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-16 06:37:48 +00:00
|
|
|
return nil, fmt.Errorf("%s: phpserialize.UnmarshalAssociativeArray: %w", url, err)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
servers := make([]Server, 0, len(m))
|
2022-07-24 16:19:27 +00:00
|
|
|
for key, val := range m {
|
|
|
|
keyStr, ok := key.(string)
|
2022-08-04 05:09:12 +00:00
|
|
|
if !ok || keyStr == "" {
|
2023-02-16 06:37:48 +00:00
|
|
|
return nil, fmt.Errorf("%s: %w: %v", url, ErrInvalidServerKey, key)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
urlStr, ok := val.(string)
|
2022-08-04 05:09:12 +00:00
|
|
|
if !ok || urlStr == "" {
|
2023-02-16 06:37:48 +00:00
|
|
|
return nil, fmt.Errorf("%s: %w: %v", url, ErrInvalidServerURL, val)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
servers = append(servers, Server{
|
2022-07-24 16:19:27 +00:00
|
|
|
Key: keyStr,
|
|
|
|
URL: urlStr,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return servers, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetServerConfig(ctx context.Context, baseURLRaw string) (ServerConfig, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
|
|
|
if err != nil {
|
|
|
|
return ServerConfig{}, err
|
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
var cfg ServerConfig
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryConfig), &cfg); err != nil {
|
|
|
|
return ServerConfig{}, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
return cfg, nil
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetBuildingInfo(ctx context.Context, baseURLRaw string) (BuildingInfo, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
|
|
|
if err != nil {
|
|
|
|
return BuildingInfo{}, err
|
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
var info BuildingInfo
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryBuildingInfo), &info); err != nil {
|
|
|
|
return BuildingInfo{}, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
return info, nil
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetUnitInfo(ctx context.Context, baseURLRaw string) (UnitInfo, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
|
|
|
if err != nil {
|
|
|
|
return UnitInfo{}, err
|
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
var info UnitInfo
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryUnitInfo), &info); err != nil {
|
|
|
|
return UnitInfo{}, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 04:16:37 +00:00
|
|
|
return info, nil
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetTribes(ctx context.Context, baseURLRaw string) ([]Tribe, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
od, err := c.getOD(ctx, baseURL, true)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
resp, err := c.get(ctx, buildURL(baseURL, endpointTribes))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
2023-02-11 07:36:56 +00:00
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return tribeCSVParser{
|
|
|
|
r: resp.Body,
|
|
|
|
baseURL: baseURL,
|
|
|
|
od: od,
|
|
|
|
}.parse()
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetPlayers(ctx context.Context, baseURLRaw string) ([]Player, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
od, err := c.getOD(ctx, baseURL, false)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
resp, err := c.get(ctx, buildURL(baseURL, endpointPlayers))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
2023-02-11 07:36:56 +00:00
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return playerCSVParser{
|
|
|
|
r: resp.Body,
|
|
|
|
baseURL: baseURL,
|
|
|
|
od: od,
|
|
|
|
}.parse()
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) getOD(ctx context.Context, baseURL *urlpkg.URL, tribe bool) (map[int64]OpponentsDefeated, error) {
|
2022-10-20 04:16:37 +00:00
|
|
|
m := make(map[int64]OpponentsDefeated)
|
2022-07-24 16:19:27 +00:00
|
|
|
urls := buildODURLs(baseURL, tribe)
|
|
|
|
|
|
|
|
for _, u := range urls {
|
2023-02-11 07:36:56 +00:00
|
|
|
if u == nil {
|
2022-07-24 16:19:27 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
records, err := c.getSingleODFile(ctx, u)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, rec := range records {
|
2023-02-11 07:36:56 +00:00
|
|
|
od := m[rec.ID]
|
2022-07-24 16:19:27 +00:00
|
|
|
|
|
|
|
switch u {
|
|
|
|
case urls[0]:
|
2023-02-11 07:36:56 +00:00
|
|
|
od.RankTotal = rec.Rank
|
|
|
|
od.ScoreTotal = rec.Score
|
2022-07-24 16:19:27 +00:00
|
|
|
case urls[1]:
|
2023-02-11 07:36:56 +00:00
|
|
|
od.RankAtt = rec.Rank
|
|
|
|
od.ScoreAtt = rec.Score
|
2022-07-24 16:19:27 +00:00
|
|
|
case urls[2]:
|
2023-02-11 07:36:56 +00:00
|
|
|
od.RankDef = rec.Rank
|
|
|
|
od.ScoreDef = rec.Score
|
2022-07-24 16:19:27 +00:00
|
|
|
case urls[3]:
|
2023-02-11 07:36:56 +00:00
|
|
|
od.RankSup = rec.Rank
|
|
|
|
od.ScoreSup = rec.Score
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
m[rec.ID] = od
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) getSingleODFile(ctx context.Context, url *urlpkg.URL) ([]odRecord, error) {
|
|
|
|
resp, err := c.get(ctx, url)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
2023-02-11 07:36:56 +00:00
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
2022-07-24 16:19:27 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return odCSVParser{
|
|
|
|
r: resp.Body,
|
|
|
|
}.parse()
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetVillages(ctx context.Context, baseURLRaw string) ([]Village, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
2022-08-21 05:19:17 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
resp, err := c.get(ctx, buildURL(baseURL, endpointVillages))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
2023-02-11 07:36:56 +00:00
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
2022-08-21 05:19:17 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return villageCSVParser{
|
|
|
|
r: resp.Body,
|
|
|
|
baseURL: baseURL,
|
|
|
|
}.parse()
|
|
|
|
}
|
2022-08-21 05:19:17 +00:00
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) GetEnnoblements(ctx context.Context, baseURLRaw string, since time.Time) ([]Ennoblement, error) {
|
|
|
|
baseURL, err := urlpkg.Parse(baseURLRaw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-23 06:45:52 +00:00
|
|
|
url := buildURL(baseURL, endpointMapEnnoblements)
|
|
|
|
if c.ennoblementsUseInterfaceFunc(since) {
|
|
|
|
url = buildURLWithQuery(baseURL, endpointInterface, fmt.Sprintf(queryInterfaceEnnoblements, since.Unix()))
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.get(ctx, url)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return ennoblementCSVParser{
|
|
|
|
r: resp.Body,
|
|
|
|
since: since,
|
|
|
|
}.parse()
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) getXML(ctx context.Context, url *urlpkg.URL, v any) error {
|
2022-07-24 16:19:27 +00:00
|
|
|
resp, err := c.get(ctx, url)
|
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return err
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
if err := xml.NewDecoder(resp.Body).Decode(v); err != nil {
|
|
|
|
return fmt.Errorf("xml.Decode: %w", err)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (c *Client) get(ctx context.Context, url *urlpkg.URL) (*http.Response, error) {
|
|
|
|
urlStr := url.String()
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
|
2022-07-24 16:19:27 +00:00
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, fmt.Errorf("%s: http.NewRequestWithContext: %w", urlStr, err)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// headers
|
|
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
|
|
|
|
|
|
resp, err := c.client.Do(req)
|
|
|
|
if err != nil {
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, fmt.Errorf("%s: client.Do: %w", urlStr, err)
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 05:34:46 +00:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
|
|
_ = resp.Body.Close()
|
2023-02-11 07:36:56 +00:00
|
|
|
return nil, fmt.Errorf("%s: got non-ok HTTP status: %d", urlStr, resp.StatusCode)
|
2022-09-12 05:34:46 +00:00
|
|
|
}
|
|
|
|
|
2022-07-24 16:19:27 +00:00
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
type tribeCSVParser struct {
|
|
|
|
r io.Reader
|
|
|
|
baseURL *urlpkg.URL
|
|
|
|
od map[int64]OpponentsDefeated
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t tribeCSVParser) parse() ([]Tribe, error) {
|
|
|
|
csvR := newCSVReader(t.r, fieldsPerRecordTribe)
|
|
|
|
|
|
|
|
//nolint:prealloc
|
|
|
|
var tribes []Tribe
|
|
|
|
for {
|
|
|
|
rec, err := csvR.Read()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tribe, err := t.parseRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tribes = append(tribes, tribe)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(tribes, func(i, j int) bool {
|
|
|
|
return tribes[i].ID < tribes[j].ID
|
|
|
|
})
|
|
|
|
|
|
|
|
return tribes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t tribeCSVParser) parseRecord(record []string) (Tribe, error) {
|
2022-08-16 05:20:44 +00:00
|
|
|
var err error
|
2022-10-20 04:16:37 +00:00
|
|
|
var tribe Tribe
|
2022-08-16 05:20:44 +00:00
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.ID, err = strconv.ParseInt(record[0], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.ID")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
tribe.Name, err = urlpkg.QueryUnescape(record[1])
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.Name")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
tribe.Tag, err = urlpkg.QueryUnescape(record[2])
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.Tag")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.NumMembers, err = strconv.ParseInt(record[3], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.NumMembers")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.NumVillages, err = strconv.ParseInt(record[4], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.NumVillages")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.Points, err = strconv.ParseInt(record[5], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.Points")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.AllPoints, err = strconv.ParseInt(record[6], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.AllPoints")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
tribe.Rank, err = strconv.ParseInt(record[7], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Tribe{}, NewParseError(err, record, "tribe.Rank")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
tribe.OpponentsDefeated = t.od[tribe.ID]
|
|
|
|
tribe.ProfileURL = buildURLWithQuery(t.baseURL, endpointGame, fmt.Sprintf(queryTribeProfile, tribe.ID)).String()
|
2022-08-16 05:20:44 +00:00
|
|
|
|
|
|
|
return tribe, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
type playerCSVParser struct {
|
|
|
|
r io.Reader
|
|
|
|
od map[int64]OpponentsDefeated
|
|
|
|
baseURL *urlpkg.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p playerCSVParser) parse() ([]Player, error) {
|
|
|
|
csvR := newCSVReader(p.r, fieldsPerRecordPlayer)
|
|
|
|
|
|
|
|
//nolint:prealloc
|
|
|
|
var players []Player
|
|
|
|
for {
|
|
|
|
rec, err := csvR.Read()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
player, err := p.parseRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
players = append(players, player)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(players, func(i, j int) bool {
|
|
|
|
return players[i].ID < players[j].ID
|
|
|
|
})
|
|
|
|
|
|
|
|
return players, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p playerCSVParser) parseRecord(record []string) (Player, error) {
|
2022-08-16 05:20:44 +00:00
|
|
|
var err error
|
2022-10-20 04:16:37 +00:00
|
|
|
var player Player
|
2022-08-16 05:20:44 +00:00
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
player.ID, err = strconv.ParseInt(record[0], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.ID")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
player.Name, err = urlpkg.QueryUnescape(record[1])
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.Name")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
player.TribeID, err = strconv.ParseInt(record[2], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.TribeID")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
player.NumVillages, err = strconv.ParseInt(record[3], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.NumVillages")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
player.Points, err = strconv.ParseInt(record[4], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.Points")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
player.Rank, err = strconv.ParseInt(record[5], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Player{}, NewParseError(err, record, "player.Rank")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
player.OpponentsDefeated = p.od[player.ID]
|
|
|
|
player.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryPlayerProfile, player.ID)).String()
|
2022-08-16 05:20:44 +00:00
|
|
|
|
|
|
|
return player, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
type villageCSVParser struct {
|
|
|
|
r io.Reader
|
|
|
|
baseURL *urlpkg.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v villageCSVParser) parse() ([]Village, error) {
|
|
|
|
csvR := newCSVReader(v.r, fieldsPerRecordVillage)
|
|
|
|
|
|
|
|
//nolint:prealloc
|
|
|
|
var villages []Village
|
|
|
|
for {
|
|
|
|
rec, err := csvR.Read()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
village, err := v.parseRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
villages = append(villages, village)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(villages, func(i, j int) bool {
|
|
|
|
return villages[i].ID < villages[j].ID
|
|
|
|
})
|
|
|
|
|
|
|
|
return villages, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v villageCSVParser) parseRecord(record []string) (Village, error) {
|
2022-08-16 05:20:44 +00:00
|
|
|
var err error
|
2022-10-20 04:16:37 +00:00
|
|
|
var village Village
|
2022-08-16 05:20:44 +00:00
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.ID, err = strconv.ParseInt(record[0], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.ID")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
village.Name, err = urlpkg.QueryUnescape(record[1])
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.Name")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.X, err = strconv.ParseInt(record[2], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.X")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.Y, err = strconv.ParseInt(record[3], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.Y")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.PlayerID, err = strconv.ParseInt(record[4], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.PlayerID")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.Points, err = strconv.ParseInt(record[5], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.Points")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
village.Bonus, err = strconv.ParseInt(record[6], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Village{}, NewParseError(err, record, "village.Bonus")
|
2022-08-16 05:20:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
village.Continent = "K" + string(record[3][0]) + string(record[2][0])
|
2023-02-11 07:36:56 +00:00
|
|
|
village.ProfileURL = buildURLWithQuery(v.baseURL, endpointGame, fmt.Sprintf(queryVillageProfile, village.ID)).String()
|
2022-08-16 05:20:44 +00:00
|
|
|
|
|
|
|
return village, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
type odCSVParser struct {
|
|
|
|
r io.Reader
|
|
|
|
}
|
|
|
|
|
2022-08-16 05:20:44 +00:00
|
|
|
type odRecord struct {
|
|
|
|
ID int64
|
|
|
|
Rank int64
|
|
|
|
Score int64
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (o odCSVParser) parse() ([]odRecord, error) {
|
|
|
|
csvR := newCSVReader(o.r, fieldsPerRecordOD)
|
|
|
|
|
|
|
|
//nolint:prealloc
|
|
|
|
var odRecords []odRecord
|
|
|
|
for {
|
|
|
|
rec, err := csvR.Read()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
od, err := o.parseRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
odRecords = append(odRecords, od)
|
|
|
|
}
|
|
|
|
|
|
|
|
return odRecords, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o odCSVParser) parseRecord(record []string) (odRecord, error) {
|
2022-08-16 05:20:44 +00:00
|
|
|
var err error
|
|
|
|
var rec odRecord
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
rec.Rank, err = strconv.ParseInt(record[0], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return odRecord{}, NewParseError(err, record, "odRecord.Rank")
|
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
rec.ID, err = strconv.ParseInt(record[1], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return odRecord{}, NewParseError(err, record, "odRecord.ID")
|
|
|
|
}
|
|
|
|
|
2022-08-21 05:19:17 +00:00
|
|
|
rec.Score, err = strconv.ParseInt(record[2], 10, 64)
|
2022-08-16 05:20:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return odRecord{}, NewParseError(err, record, "odRecord.Score")
|
|
|
|
}
|
|
|
|
|
|
|
|
return rec, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
type ennoblementCSVParser struct {
|
|
|
|
r io.Reader
|
|
|
|
since time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e ennoblementCSVParser) parse() ([]Ennoblement, error) {
|
|
|
|
csvR := newCSVReader(e.r, fieldsPerRecordEnnoblement)
|
|
|
|
|
|
|
|
//nolint:prealloc
|
|
|
|
var ennoblements []Ennoblement
|
|
|
|
for {
|
|
|
|
rec, err := csvR.Read()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ennoblement, err := e.parseRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if ennoblement.CreatedAt.Before(e.since) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ennoblements = append(ennoblements, ennoblement)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(ennoblements, func(i, j int) bool {
|
|
|
|
return ennoblements[i].CreatedAt.Before(ennoblements[j].CreatedAt)
|
|
|
|
})
|
|
|
|
|
|
|
|
return ennoblements, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error) {
|
2022-08-21 05:19:17 +00:00
|
|
|
var err error
|
2022-10-20 04:16:37 +00:00
|
|
|
var ennoblement Ennoblement
|
2022-08-21 05:19:17 +00:00
|
|
|
|
|
|
|
ennoblement.VillageID, err = strconv.ParseInt(record[0], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.VillageID")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
ennoblement.CreatedAt, err = e.parseTimestamp(record[1])
|
2022-08-21 05:19:17 +00:00
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.CreatedAt")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ennoblement.NewOwnerID, err = strconv.ParseInt(record[2], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.NewOwnerID")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ennoblement.OldOwnerID, err = strconv.ParseInt(record[3], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.OldOwnerID")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ennoblement.OldTribeID, err = strconv.ParseInt(record[4], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.OldTribeID")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ennoblement.NewTribeID, err = strconv.ParseInt(record[5], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.NewTribeID")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ennoblement.Points, err = strconv.ParseInt(record[6], 10, 64)
|
|
|
|
if err != nil {
|
2022-10-20 04:16:37 +00:00
|
|
|
return Ennoblement{}, NewParseError(err, record, "ennoblement.Points")
|
2022-08-21 05:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ennoblement, nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func (e ennoblementCSVParser) parseTimestamp(s string) (time.Time, error) {
|
2022-08-21 05:19:17 +00:00
|
|
|
timestamp, err := strconv.ParseInt(s, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return time.Time{}, err
|
|
|
|
}
|
|
|
|
return time.Unix(timestamp, 0), nil
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func newCSVReader(r io.Reader, fieldsPerRecord int) *csv.Reader {
|
|
|
|
csvR := csv.NewReader(r)
|
|
|
|
csvR.Comma = ','
|
|
|
|
csvR.FieldsPerRecord = fieldsPerRecord
|
|
|
|
csvR.ReuseRecord = true
|
|
|
|
return csvR
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildODURLs(base *urlpkg.URL, tribe bool) [4]*urlpkg.URL {
|
2022-08-08 04:24:23 +00:00
|
|
|
if tribe {
|
2023-02-11 07:36:56 +00:00
|
|
|
return [4]*urlpkg.URL{
|
2022-08-08 04:24:23 +00:00
|
|
|
buildURL(base, endpointTribesOD),
|
|
|
|
buildURL(base, endpointTribesODA),
|
|
|
|
buildURL(base, endpointTribesODD),
|
2023-02-11 07:36:56 +00:00
|
|
|
nil,
|
2022-08-08 04:24:23 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-11 07:36:56 +00:00
|
|
|
return [4]*urlpkg.URL{
|
2022-08-08 04:24:23 +00:00
|
|
|
buildURL(base, endpointPlayersOD),
|
|
|
|
buildURL(base, endpointPlayersODA),
|
|
|
|
buildURL(base, endpointPlayersODD),
|
|
|
|
buildURL(base, endpointPlayersODS),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 07:36:56 +00:00
|
|
|
func buildURL(base *urlpkg.URL, path string) *urlpkg.URL {
|
|
|
|
return buildURLWithQuery(base, path, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildURLWithQuery(base *urlpkg.URL, path, query string) *urlpkg.URL {
|
|
|
|
return &urlpkg.URL{
|
|
|
|
Scheme: base.Scheme,
|
|
|
|
Host: base.Host,
|
|
|
|
Path: path,
|
|
|
|
RawQuery: query,
|
2022-07-24 16:19:27 +00:00
|
|
|
}
|
|
|
|
}
|