Dawid Wysokiński
0aed6f8fed
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: twhelp/core#106
244 lines
6.3 KiB
Go
244 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
|
"gitea.dwysokinski.me/twhelp/core/internal/tw"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
const (
|
|
playerChunkSize = 1000
|
|
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
|
|
DeleteByID(ctx context.Context, ids ...int64) error
|
|
List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, int64, error)
|
|
}
|
|
|
|
//counterfeiter:generate -o internal/mock/player_getter.gen.go . PlayerGetter
|
|
type PlayerGetter interface {
|
|
GetPlayers(ctx context.Context, baseURL string) ([]tw.Player, error)
|
|
}
|
|
|
|
type Player struct {
|
|
repo PlayerRepository
|
|
client PlayerGetter
|
|
}
|
|
|
|
func NewPlayer(repo PlayerRepository, client PlayerGetter) *Player {
|
|
return &Player{repo: repo, client: client}
|
|
}
|
|
|
|
func (p *Player) Refresh(ctx context.Context, key, url string) (int64, error) {
|
|
ctx, span := tracer.Start(ctx, "Player.Refresh", trace.WithAttributes(
|
|
attribute.String("server.key", key),
|
|
attribute.String("server.url", url),
|
|
))
|
|
defer span.End()
|
|
|
|
players, err := p.client.GetPlayers(ctx, url)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return 0, fmt.Errorf("TWClient.GetPlayers: %w", err)
|
|
}
|
|
|
|
if err := p.createOrUpdate(ctx, key, players); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return 0, err
|
|
}
|
|
|
|
if err := p.delete(ctx, key, players); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return 0, err
|
|
}
|
|
|
|
return int64(len(players)), nil
|
|
}
|
|
|
|
func (p *Player) createOrUpdate(ctx context.Context, key string, players []tw.Player) error {
|
|
ctx, span := tracer.Start(ctx, "Player.createOrUpdate", trace.WithAttributes(
|
|
attribute.String("server.key", key),
|
|
))
|
|
defer span.End()
|
|
|
|
for i := 0; i < len(players); i += playerChunkSize {
|
|
end := i + playerChunkSize
|
|
if end > len(players) {
|
|
end = len(players)
|
|
}
|
|
|
|
chunk := players[i:end]
|
|
|
|
params := make([]domain.CreatePlayerParams, 0, len(chunk))
|
|
for _, player := range chunk {
|
|
params = append(params, domain.CreatePlayerParams{
|
|
OpponentsDefeated: domain.OpponentsDefeated(player.OpponentsDefeated),
|
|
ID: player.ID,
|
|
Name: player.Name,
|
|
NumVillages: player.NumVillages,
|
|
Points: player.Points,
|
|
Rank: player.Rank,
|
|
TribeID: player.TribeID,
|
|
ProfileURL: player.ProfileURL,
|
|
ServerKey: key,
|
|
})
|
|
}
|
|
if err := p.repo.CreateOrUpdate(ctx, params...); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return fmt.Errorf("PlayerRepository.CreateOrUpdate: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Player) delete(ctx context.Context, key string, existing []tw.Player) error {
|
|
ctx, span := tracer.Start(ctx, "Player.delete", trace.WithAttributes(
|
|
attribute.String("server.key", key),
|
|
))
|
|
defer span.End()
|
|
|
|
//nolint:prealloc
|
|
var playersToDelete []int64
|
|
var lastID int64
|
|
for {
|
|
players, _, 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 {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
|
|
for _, player := range players {
|
|
i := sort.Search(len(existing), func(i int) bool {
|
|
return existing[i].ID >= player.ID
|
|
})
|
|
if i < len(existing) && existing[i].ID == player.ID {
|
|
continue
|
|
}
|
|
playersToDelete = append(playersToDelete, player.ID)
|
|
}
|
|
|
|
if len(players) < playerChunkSize {
|
|
break
|
|
}
|
|
|
|
lastID = players[len(players)-1].ID
|
|
}
|
|
|
|
if err := p.repo.DeleteByID(ctx, playersToDelete...); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return fmt.Errorf("PlayerRepository.DeleteByID: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Player) List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, int64, error) {
|
|
ctx, span := tracer.Start(ctx, "Player.List")
|
|
defer span.End()
|
|
|
|
if len(params.Sort) == 0 {
|
|
params.Sort = []domain.PlayerSort{
|
|
{
|
|
By: domain.PlayerSortByID,
|
|
Direction: domain.SortDirectionASC,
|
|
},
|
|
}
|
|
}
|
|
|
|
if len(params.Sort) > playerSortMaxLen {
|
|
err := domain.ValidationError{
|
|
Field: "sort",
|
|
Err: domain.MaxLengthError{
|
|
Max: playerSortMaxLen,
|
|
},
|
|
}
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return nil, 0, err
|
|
}
|
|
|
|
if params.Pagination.Limit == 0 {
|
|
params.Pagination.Limit = playerMaxLimit
|
|
}
|
|
|
|
if err := validatePagination(params.Pagination, playerMaxLimit); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return nil, 0, fmt.Errorf("validatePagination: %w", err)
|
|
}
|
|
|
|
players, count, err := p.repo.List(ctx, params)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return nil, 0, fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
|
|
return players, count, nil
|
|
}
|
|
|
|
func (p *Player) GetByServerKeyAndID(ctx context.Context, serverKey string, id int64, includeTribe bool) (domain.Player, error) {
|
|
ctx, span := tracer.Start(ctx, "Player.GetByServerKeyAndID", trace.WithAttributes(
|
|
attribute.String("server.key", serverKey),
|
|
attribute.Int64("player.id", id),
|
|
))
|
|
defer span.End()
|
|
|
|
players, _, err := p.repo.List(ctx, domain.ListPlayersParams{
|
|
IDs: []int64{id},
|
|
ServerKeys: []string{serverKey},
|
|
IncludeTribe: includeTribe,
|
|
Pagination: domain.Pagination{
|
|
Limit: 1,
|
|
},
|
|
})
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return domain.Player{}, fmt.Errorf("PlayerRepository.List: %w", err)
|
|
}
|
|
if len(players) == 0 {
|
|
err = domain.PlayerNotFoundError{
|
|
ID: id,
|
|
}
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return domain.Player{}, err
|
|
}
|
|
|
|
return players[0], nil
|
|
}
|