Dawid Wysokiński
6c8b30d061
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: twhelp/core#144
368 lines
9.7 KiB
Go
368 lines
9.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
|
"gitea.dwysokinski.me/twhelp/core/internal/tw"
|
|
)
|
|
|
|
const (
|
|
playerChunkSize = 500
|
|
playerMaxLimit = 200
|
|
playerSortMaxLen = 3
|
|
)
|
|
|
|
//counterfeiter:generate -o internal/mock/player_repository.gen.go . PlayerRepository
|
|
type PlayerRepository interface {
|
|
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
|
|
Delete(ctx context.Context, serverKey string, ids ...int64) error
|
|
List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, error)
|
|
ListCountWithRelations(ctx context.Context, params domain.ListPlayersParams) ([]domain.PlayerWithRelations, int64, error)
|
|
}
|
|
|
|
//counterfeiter:generate -o internal/mock/player_getter.gen.go . PlayerGetter
|
|
type PlayerGetter interface {
|
|
GetPlayers(ctx context.Context, baseURL string) ([]tw.Player, error)
|
|
}
|
|
|
|
//counterfeiter:generate -o internal/mock/tribe_change_creator.gen.go . TribeChangeCreator
|
|
type TribeChangeCreator interface {
|
|
Create(ctx context.Context, params ...domain.CreateTribeChangeParams) error
|
|
}
|
|
|
|
type Player struct {
|
|
repo PlayerRepository
|
|
tribeChangeSvc TribeChangeCreator
|
|
client PlayerGetter
|
|
}
|
|
|
|
func NewPlayer(repo PlayerRepository, tribeChangeSvc TribeChangeCreator, client PlayerGetter) *Player {
|
|
return &Player{repo: repo, tribeChangeSvc: tribeChangeSvc, client: client}
|
|
}
|
|
|
|
func (p *Player) Refresh(ctx context.Context, key, url string) (int64, error) {
|
|
players, err := p.client.GetPlayers(ctx, url)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("TWClient.GetPlayers: %w", err)
|
|
}
|
|
|
|
if err = p.createOrUpdate(ctx, key, players); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err = p.delete(ctx, key, players); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return int64(len(players)), nil
|
|
}
|
|
|
|
func (p *Player) createOrUpdate(ctx context.Context, key string, players []tw.Player) error {
|
|
for i := 0; i < len(players); i += playerChunkSize {
|
|
end := i + playerChunkSize
|
|
if end > len(players) {
|
|
end = len(players)
|
|
}
|
|
|
|
if err := p.createOrUpdateChunk(ctx, key, players[i:end]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Player) createOrUpdateChunk(ctx context.Context, key string, chunk []tw.Player) error {
|
|
ids := make([]int64, 0, len(chunk))
|
|
for _, player := range chunk {
|
|
ids = append(ids, player.ID)
|
|
}
|
|
|
|
playersDB, err := p.repo.List(ctx, domain.ListPlayersParams{
|
|
ServerKeys: []string{key},
|
|
IDs: ids,
|
|
Sort: []domain.PlayerSort{
|
|
{By: domain.PlayerSortByID, Direction: domain.SortDirectionASC},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
|
|
tribeChanges := make([]domain.CreateTribeChangeParams, 0, countTribeChanges(chunk, playersDB))
|
|
params := make([]domain.CreatePlayerParams, 0, len(chunk))
|
|
for _, player := range chunk {
|
|
var playerDB domain.Player
|
|
j := sort.Search(len(playersDB), func(i int) bool {
|
|
return playersDB[i].ID >= player.ID
|
|
})
|
|
if j < len(playersDB) && playersDB[j].ID == player.ID {
|
|
playerDB = playersDB[j]
|
|
}
|
|
|
|
if playerDB.TribeID != player.TribeID {
|
|
tribeChanges = append(tribeChanges, domain.CreateTribeChangeParams{
|
|
PlayerID: player.ID,
|
|
NewTribeID: player.TribeID,
|
|
OldTribeID: playerDB.TribeID,
|
|
ServerKey: key,
|
|
})
|
|
}
|
|
|
|
params = append(params, createPlayerParamsBuilder{player, playerDB, key}.build())
|
|
}
|
|
|
|
if err = p.repo.CreateOrUpdate(ctx, params...); err != nil {
|
|
return fmt.Errorf("PlayerRepository.CreateOrUpdate: %w", err)
|
|
}
|
|
|
|
if err = p.tribeChangeSvc.Create(ctx, tribeChanges...); err != nil {
|
|
return fmt.Errorf("TribeChangeService.Create: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Player) delete(ctx context.Context, key string, players []tw.Player) error {
|
|
//nolint:prealloc
|
|
var playersToDelete []int64
|
|
var tribeChanges []domain.CreateTribeChangeParams
|
|
|
|
var lastID int64
|
|
for {
|
|
playersDB, err := p.repo.List(ctx, domain.ListPlayersParams{
|
|
ServerKeys: []string{key},
|
|
IDGT: domain.NullInt64{
|
|
Int64: lastID,
|
|
Valid: true,
|
|
},
|
|
Deleted: domain.NullBool{
|
|
Valid: true,
|
|
Bool: false,
|
|
},
|
|
Pagination: domain.Pagination{
|
|
Limit: playerChunkSize,
|
|
},
|
|
Sort: []domain.PlayerSort{
|
|
{By: domain.PlayerSortByID, Direction: domain.SortDirectionASC},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
|
|
for _, player := range playersDB {
|
|
i := sort.Search(len(players), func(i int) bool {
|
|
return players[i].ID >= player.ID
|
|
})
|
|
if i < len(players) && players[i].ID == player.ID {
|
|
continue
|
|
}
|
|
|
|
if player.TribeID > 0 {
|
|
tribeChanges = append(tribeChanges, domain.CreateTribeChangeParams{
|
|
PlayerID: player.ID,
|
|
NewTribeID: 0,
|
|
OldTribeID: player.TribeID,
|
|
ServerKey: key,
|
|
})
|
|
}
|
|
|
|
playersToDelete = append(playersToDelete, player.ID)
|
|
}
|
|
|
|
if len(playersDB) < playerChunkSize {
|
|
break
|
|
}
|
|
|
|
lastID = playersDB[len(playersDB)-1].ID
|
|
}
|
|
|
|
if err := p.repo.Delete(ctx, key, playersToDelete...); err != nil {
|
|
return fmt.Errorf("PlayerRepository.Delete: %w", err)
|
|
}
|
|
|
|
if err := p.tribeChangeSvc.Create(ctx, tribeChanges...); err != nil {
|
|
return fmt.Errorf("TribeChangeService.Create: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Player) List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, error) {
|
|
params, err := listPlayersParamsBuilder{params}.build()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
players, err := p.repo.List(ctx, params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
|
|
return players, nil
|
|
}
|
|
|
|
func (p *Player) ListCountWithRelations(ctx context.Context, params domain.ListPlayersParams) ([]domain.PlayerWithRelations, int64, error) {
|
|
params, err := listPlayersParamsBuilder{params}.build()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
players, count, err := p.repo.ListCountWithRelations(ctx, params)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("PlayerRepository.ListCountWithRelations: %w", err)
|
|
}
|
|
|
|
return players, count, nil
|
|
}
|
|
|
|
func (p *Player) GetByServerKeyAndIDWithRelations(ctx context.Context, serverKey string, id int64) (domain.PlayerWithRelations, error) {
|
|
players, _, err := p.repo.ListCountWithRelations(ctx, domain.ListPlayersParams{
|
|
IDs: []int64{id},
|
|
ServerKeys: []string{serverKey},
|
|
Pagination: domain.Pagination{
|
|
Limit: 1,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return domain.PlayerWithRelations{}, fmt.Errorf("PlayerRepository.ListCountWithRelations: %w", err)
|
|
}
|
|
if len(players) == 0 {
|
|
return domain.PlayerWithRelations{}, domain.PlayerNotFoundError{
|
|
ID: id,
|
|
}
|
|
}
|
|
|
|
return players[0], nil
|
|
}
|
|
|
|
type createPlayerParamsBuilder struct {
|
|
player tw.Player
|
|
playerDB domain.Player
|
|
key string
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) build() domain.CreatePlayerParams {
|
|
var p domain.CreatePlayerParams
|
|
for _, fn := range [...]func(p domain.CreatePlayerParams) domain.CreatePlayerParams{
|
|
c.setBase,
|
|
c.setBestRank,
|
|
c.setMostVillages,
|
|
c.setMostPoints,
|
|
c.setLastActivityAt,
|
|
} {
|
|
p = fn(p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) setBase(p domain.CreatePlayerParams) domain.CreatePlayerParams {
|
|
p.OpponentsDefeated = domain.OpponentsDefeated(c.player.OpponentsDefeated)
|
|
p.ID = c.player.ID
|
|
p.Name = c.player.Name
|
|
p.NumVillages = c.player.NumVillages
|
|
p.Points = c.player.Points
|
|
p.Rank = c.player.Rank
|
|
p.TribeID = c.player.TribeID
|
|
p.ProfileURL = c.player.ProfileURL
|
|
p.ServerKey = c.key
|
|
return p
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) setBestRank(p domain.CreatePlayerParams) domain.CreatePlayerParams {
|
|
if c.player.Rank >= c.playerDB.BestRank && c.playerDB.ID > 0 {
|
|
p.BestRank = c.playerDB.BestRank
|
|
p.BestRankAt = c.playerDB.BestRankAt
|
|
return p
|
|
}
|
|
p.BestRank = c.player.Rank
|
|
p.BestRankAt = time.Now()
|
|
return p
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) setMostPoints(p domain.CreatePlayerParams) domain.CreatePlayerParams {
|
|
if c.player.Points <= c.playerDB.MostPoints && c.playerDB.ID > 0 {
|
|
p.MostPoints = c.playerDB.MostPoints
|
|
p.MostPointsAt = c.playerDB.MostPointsAt
|
|
return p
|
|
}
|
|
p.MostPoints = c.player.Points
|
|
p.MostPointsAt = time.Now()
|
|
return p
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) setMostVillages(p domain.CreatePlayerParams) domain.CreatePlayerParams {
|
|
if c.player.NumVillages <= c.playerDB.MostVillages && c.playerDB.ID > 0 {
|
|
p.MostVillages = c.playerDB.MostVillages
|
|
p.MostVillagesAt = c.playerDB.MostVillagesAt
|
|
return p
|
|
}
|
|
p.MostVillages = c.player.NumVillages
|
|
p.MostVillagesAt = time.Now()
|
|
return p
|
|
}
|
|
|
|
func (c createPlayerParamsBuilder) setLastActivityAt(p domain.CreatePlayerParams) domain.CreatePlayerParams {
|
|
if c.player.Points <= c.playerDB.Points &&
|
|
c.player.NumVillages <= c.playerDB.NumVillages &&
|
|
c.player.ScoreAtt <= c.playerDB.ScoreAtt &&
|
|
c.playerDB.ID > 0 {
|
|
p.LastActivityAt = c.playerDB.LastActivityAt
|
|
return p
|
|
}
|
|
p.LastActivityAt = time.Now()
|
|
return p
|
|
}
|
|
|
|
func countTribeChanges(players []tw.Player, playersDB []domain.Player) int {
|
|
tribeChangesCnt := 0
|
|
for _, player := range players {
|
|
i := sort.Search(len(playersDB), func(i int) bool {
|
|
return playersDB[i].ID >= player.ID
|
|
})
|
|
if i < len(playersDB) && playersDB[i].ID == player.ID && playersDB[i].TribeID != player.TribeID {
|
|
tribeChangesCnt++
|
|
}
|
|
}
|
|
return tribeChangesCnt
|
|
}
|
|
|
|
type listPlayersParamsBuilder struct {
|
|
domain.ListPlayersParams
|
|
}
|
|
|
|
func (l listPlayersParamsBuilder) build() (domain.ListPlayersParams, error) {
|
|
if len(l.Sort) == 0 {
|
|
l.Sort = []domain.PlayerSort{
|
|
{
|
|
By: domain.PlayerSortByID,
|
|
Direction: domain.SortDirectionASC,
|
|
},
|
|
}
|
|
}
|
|
|
|
if l.Pagination.Limit == 0 {
|
|
l.Pagination.Limit = tribeMaxLimit
|
|
}
|
|
|
|
if len(l.Sort) > playerSortMaxLen {
|
|
return domain.ListPlayersParams{}, domain.ValidationError{
|
|
Field: "sort",
|
|
Err: domain.MaxLengthError{
|
|
Max: playerSortMaxLen,
|
|
},
|
|
}
|
|
}
|
|
|
|
if err := validatePagination(l.Pagination, playerMaxLimit); err != nil {
|
|
return domain.ListPlayersParams{}, fmt.Errorf("validatePagination: %w", err)
|
|
}
|
|
|
|
return l.ListPlayersParams, nil
|
|
}
|