Add player profiles and community settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"mixmaker/backend/internal/application"
|
||||
@@ -62,13 +63,27 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
|
||||
if err != nil {
|
||||
return domain.Account{}, domain.Player{}, err
|
||||
}
|
||||
p := domain.Player{ID: application.NewID(), AccountID: account.ID, DisplayName: account.Username, Ratings: domain.Ratings{Tank: 13, Damage: 13, Support: 13}, CreatedAt: account.CreatedAt, UpdatedAt: account.CreatedAt}
|
||||
p, err := playerByAccount(ctx, tx, account.ID)
|
||||
if err == nil {
|
||||
return account, p, tx.Commit(ctx)
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Account{}, domain.Player{}, err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('mixmaker-player-display-names'))`); err != nil {
|
||||
return domain.Account{}, domain.Player{}, err
|
||||
}
|
||||
displayName, err := uniqueDefaultDisplayName(ctx, tx, account.Username)
|
||||
if err != nil {
|
||||
return domain.Account{}, domain.Player{}, err
|
||||
}
|
||||
p = domain.Player{ID: application.NewID(), AccountID: account.ID, DisplayName: displayName, Ratings: domain.Ratings{Tank: 13, Damage: 13, Support: 13}, CreatedAt: account.CreatedAt, UpdatedAt: account.CreatedAt}
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(account_id) DO UPDATE SET account_id=excluded.account_id
|
||||
RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
||||
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,battle_tag,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
||||
VALUES($1,$2,$3,'',$4,$5,$6,$7,$8) ON CONFLICT(account_id) DO UPDATE SET account_id=excluded.account_id
|
||||
RETURNING id,account_id,display_name,battle_tag,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
||||
p.ID, p.AccountID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err != nil {
|
||||
return domain.Account{}, domain.Player{}, err
|
||||
}
|
||||
@@ -84,6 +99,51 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
|
||||
return account, p, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func playerByAccount(ctx context.Context, tx pgx.Tx, accountID string) (domain.Player, error) {
|
||||
var p domain.Player
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
err := tx.QueryRow(ctx, `SELECT id,account_id,display_name,battle_tag,tank_rating,damage_rating,support_rating,
|
||||
preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at FROM players WHERE account_id=$1`, accountID).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
|
||||
&preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
if err = json.Unmarshal(preferredRoles, &p.PreferredRoles); err == nil {
|
||||
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
|
||||
}
|
||||
if err == nil {
|
||||
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func uniqueDefaultDisplayName(ctx context.Context, tx pgx.Tx, username string) (string, error) {
|
||||
baseRunes := []rune(strings.TrimSpace(username))
|
||||
if len(baseRunes) < 2 {
|
||||
baseRunes = []rune("Player")
|
||||
}
|
||||
if len(baseRunes) > 32 {
|
||||
baseRunes = baseRunes[:32]
|
||||
}
|
||||
base := string(baseRunes)
|
||||
for number := 1; ; number++ {
|
||||
candidate := base
|
||||
if number > 1 {
|
||||
suffix := fmt.Sprintf("-%d", number)
|
||||
limit := 32 - len([]rune(suffix))
|
||||
candidate = string([]rune(base)[:min(len([]rune(base)), limit)]) + suffix
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM players WHERE lower(display_name)=lower($1))`, candidate).Scan(&exists); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) ListAccounts(ctx context.Context) ([]domain.Account, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT id,discord_id,username,avatar_url,role,created_at FROM accounts ORDER BY lower(username),id`)
|
||||
if err != nil {
|
||||
@@ -124,11 +184,11 @@ func (s *Store) AccountBySession(ctx context.Context, token string) (domain.Acco
|
||||
var p domain.Player
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
err := s.pool.QueryRow(ctx, `SELECT a.id,a.discord_id,a.username,a.avatar_url,a.role,a.created_at,
|
||||
p.id,p.account_id,p.display_name,p.tank_rating,p.damage_rating,p.support_rating,p.preferred_roles,p.preferred_player_ids,p.avoided_player_ids,p.created_at,p.updated_at
|
||||
p.id,p.account_id,p.display_name,p.battle_tag,p.tank_rating,p.damage_rating,p.support_rating,p.preferred_roles,p.preferred_player_ids,p.avoided_player_ids,p.created_at,p.updated_at
|
||||
FROM sessions s JOIN accounts a ON a.id=s.account_id JOIN players p ON p.account_id=a.id
|
||||
WHERE s.token_hash=$1 AND s.expires_at>now()`, HashToken(token)).
|
||||
Scan(&a.ID, &a.DiscordID, &a.Username, &a.AvatarURL, &a.Role, &a.CreatedAt,
|
||||
&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
|
||||
}
|
||||
@@ -145,16 +205,35 @@ func (s *Store) UpdatePlayer(ctx context.Context, p domain.Player) (domain.Playe
|
||||
preferredRoles, _ := json.Marshal(p.PreferredRoles)
|
||||
preferredPlayers, _ := json.Marshal(p.PreferredPlayerIDs)
|
||||
avoidedPlayers, _ := json.Marshal(p.AvoidedPlayerIDs)
|
||||
err := s.pool.QueryRow(ctx, `UPDATE players SET display_name=$2,tank_rating=$3,damage_rating=$4,support_rating=$5,
|
||||
preferred_roles=$6,preferred_player_ids=$7,avoided_player_ids=$8,updated_at=$9
|
||||
WHERE id=$1 RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at`,
|
||||
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, preferredRoles, preferredPlayers, avoidedPlayers, p.UpdatedAt).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &p.CreatedAt, &p.UpdatedAt)
|
||||
err := s.pool.QueryRow(ctx, `UPDATE players SET display_name=$2,battle_tag=$3,tank_rating=$4,damage_rating=$5,support_rating=$6,
|
||||
preferred_roles=$7,preferred_player_ids=$8,avoided_player_ids=$9,updated_at=$10
|
||||
WHERE id=$1 RETURNING id,COALESCE(account_id,''),display_name,battle_tag,tank_rating,damage_rating,support_rating,created_at,updated_at`,
|
||||
p.ID, p.DisplayName, p.BattleTag, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, preferredRoles, preferredPlayers, avoidedPlayers, p.UpdatedAt).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &p.CreatedAt, &p.UpdatedAt)
|
||||
return p, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) GetPlayer(ctx context.Context, id string) (domain.Player, error) {
|
||||
var p domain.Player
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
err := s.pool.QueryRow(ctx, `SELECT id,COALESCE(account_id,''),display_name,battle_tag,tank_rating,damage_rating,support_rating,
|
||||
preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at FROM players WHERE id=$1`, id).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
|
||||
&preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
|
||||
}
|
||||
if err == nil {
|
||||
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
|
||||
}
|
||||
if err == nil {
|
||||
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
|
||||
}
|
||||
return p, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,
|
||||
rows, err := s.pool.Query(ctx, `SELECT id,COALESCE(account_id,''),display_name,battle_tag,tank_rating,damage_rating,support_rating,
|
||||
preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at FROM players ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -164,7 +243,7 @@ func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
|
||||
for rows.Next() {
|
||||
var p domain.Player
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
if err := rows.Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
|
||||
if err := rows.Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
|
||||
&preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,11 +268,11 @@ func (s *Store) CreateParticipant(ctx context.Context, p domain.Player, r domain
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
||||
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
||||
VALUES($1,NULL,$2,$3,$4,$5,$6,$7)
|
||||
RETURNING id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
||||
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,battle_tag,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
||||
VALUES($1,NULL,$2,$3,$4,$5,$6,$7,$8)
|
||||
RETURNING id,COALESCE(account_id,''),display_name,battle_tag,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
||||
p.ID, p.DisplayName, p.BattleTag, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
|
||||
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.BattleTag, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
|
||||
}
|
||||
@@ -638,6 +717,27 @@ func (s *Store) GetSeries(ctx context.Context, id string) (domain.Series, error)
|
||||
return out, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) ListSeriesByEvent(ctx context.Context, eventID string) ([]domain.Series, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT body FROM series WHERE body->>'eventId'=$1 ORDER BY id`, eventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.Series, 0)
|
||||
for rows.Next() {
|
||||
var body []byte
|
||||
var series domain.Series
|
||||
if err := rows.Scan(&body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &series); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, series)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) SaveTournament(ctx context.Context, t domain.Tournament) (domain.Tournament, error) {
|
||||
body, _ := json.Marshal(t)
|
||||
tag, err := s.pool.Exec(ctx, `INSERT INTO tournaments(id,event_id,body,version) VALUES($1,$2,$3,$4)
|
||||
@@ -749,6 +849,20 @@ func (s *Store) GetDraft(ctx context.Context, id string, target any) (string, in
|
||||
return kind, version, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) GetCommunitySettings(ctx context.Context) (domain.CommunitySettings, error) {
|
||||
var settings domain.CommunitySettings
|
||||
err := s.pool.QueryRow(ctx, `SELECT discord_invite_url,updated_at,COALESCE(updated_by,'') FROM community_settings WHERE singleton=true`).
|
||||
Scan(&settings.DiscordInviteURL, &settings.UpdatedAt, &settings.UpdatedBy)
|
||||
return settings, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateCommunitySettings(ctx context.Context, settings domain.CommunitySettings) (domain.CommunitySettings, error) {
|
||||
err := s.pool.QueryRow(ctx, `UPDATE community_settings SET discord_invite_url=$1,updated_at=$2,updated_by=$3 WHERE singleton=true
|
||||
RETURNING discord_invite_url,updated_at,updated_by`, settings.DiscordInviteURL, settings.UpdatedAt, settings.UpdatedBy).
|
||||
Scan(&settings.DiscordInviteURL, &settings.UpdatedAt, &settings.UpdatedBy)
|
||||
return settings, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) AppendAudit(ctx context.Context, actor, action, subject string, payload any) error {
|
||||
body, _ := json.Marshal(payload)
|
||||
_, err := s.pool.Exec(ctx, `INSERT INTO audit_log(actor_account_id,action,subject_id,payload) VALUES($1,$2,$3,$4)`, actor, action, subject, body)
|
||||
@@ -806,5 +920,9 @@ func mapError(err error) error {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ErrNotFound
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return fmt.Errorf("%w: value already exists", domain.ErrConflict)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user