Add player profiles and community settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,7 @@ type Store interface {
|
||||
CreateSession(context.Context, string, string, time.Time) error
|
||||
DeleteSession(context.Context, string) error
|
||||
UpdatePlayer(context.Context, domain.Player) (domain.Player, error)
|
||||
GetPlayer(context.Context, string) (domain.Player, error)
|
||||
ListPlayers(context.Context) ([]domain.Player, error)
|
||||
EnqueueDiscordRoleSync(context.Context, string, string, int64) error
|
||||
CreateParticipant(context.Context, domain.Player, domain.RSVP) (domain.Player, domain.RSVP, error)
|
||||
@@ -49,11 +50,14 @@ type Store interface {
|
||||
GetRuleset(context.Context, string) (domain.Ruleset, error)
|
||||
SaveSeries(context.Context, domain.Series) (domain.Series, error)
|
||||
GetSeries(context.Context, string) (domain.Series, error)
|
||||
ListSeriesByEvent(context.Context, string) ([]domain.Series, error)
|
||||
SaveTournament(context.Context, domain.Tournament) (domain.Tournament, error)
|
||||
GetTournament(context.Context, string) (domain.Tournament, error)
|
||||
GetTournamentByEvent(context.Context, string) (domain.Tournament, error)
|
||||
SaveDraft(context.Context, string, string, any, int) error
|
||||
GetDraft(context.Context, string, any) (string, int, error)
|
||||
GetCommunitySettings(context.Context) (domain.CommunitySettings, error)
|
||||
UpdateCommunitySettings(context.Context, domain.CommunitySettings) (domain.CommunitySettings, error)
|
||||
AppendAudit(context.Context, string, string, string, any) error
|
||||
}
|
||||
|
||||
@@ -125,20 +129,35 @@ func (s *Service) SetModerator(ctx context.Context, actor domain.Account, accoun
|
||||
return account, err
|
||||
}
|
||||
|
||||
func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, current domain.Player, displayName string, ratings domain.Ratings, preferredRoles []domain.Role, preferredPlayerIDs, avoidedPlayerIDs []string) (domain.Player, error) {
|
||||
type UpdateOwnProfileInput struct {
|
||||
DisplayName *string
|
||||
BattleTag *string
|
||||
Ratings domain.Ratings
|
||||
PreferredRoles []domain.Role
|
||||
PreferredPlayerIDs []string
|
||||
AvoidedPlayerIDs []string
|
||||
}
|
||||
|
||||
func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, current domain.Player, input UpdateOwnProfileInput) (domain.Player, error) {
|
||||
if actor.ID != current.AccountID {
|
||||
return domain.Player{}, domain.ErrForbidden
|
||||
}
|
||||
if err := ratings.Validate(); err != nil {
|
||||
if err := input.Ratings.Validate(); err != nil {
|
||||
return domain.Player{}, err
|
||||
}
|
||||
if displayName != "" {
|
||||
current.DisplayName = displayName
|
||||
if input.DisplayName != nil {
|
||||
current.DisplayName = *input.DisplayName
|
||||
}
|
||||
current.Ratings = ratings
|
||||
current.PreferredRoles = preferredRoles
|
||||
current.PreferredPlayerIDs = preferredPlayerIDs
|
||||
current.AvoidedPlayerIDs = avoidedPlayerIDs
|
||||
if input.BattleTag != nil {
|
||||
current.BattleTag = *input.BattleTag
|
||||
}
|
||||
if err := current.ValidateIdentity(); err != nil {
|
||||
return domain.Player{}, err
|
||||
}
|
||||
current.Ratings = input.Ratings
|
||||
current.PreferredRoles = input.PreferredRoles
|
||||
current.PreferredPlayerIDs = input.PreferredPlayerIDs
|
||||
current.AvoidedPlayerIDs = input.AvoidedPlayerIDs
|
||||
current.UpdatedAt = s.Now()
|
||||
if err := current.ValidatePreferences(); err != nil {
|
||||
return domain.Player{}, err
|
||||
@@ -168,6 +187,75 @@ func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, cu
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) GetPlayerProfile(ctx context.Context, playerID string) (domain.PlayerProfile, error) {
|
||||
player, err := s.Store.GetPlayer(ctx, playerID)
|
||||
if err != nil {
|
||||
return domain.PlayerProfile{}, err
|
||||
}
|
||||
events, err := s.Store.ListEvents(ctx, time.Unix(0, 0).UTC())
|
||||
if err != nil {
|
||||
return domain.PlayerProfile{}, err
|
||||
}
|
||||
matches := make([]domain.PlayerSeriesContext, 0)
|
||||
for _, event := range events {
|
||||
if event.State != domain.Completed {
|
||||
continue
|
||||
}
|
||||
teams, listErr := s.Store.ListTeams(ctx, event.ID)
|
||||
if listErr != nil {
|
||||
return domain.PlayerProfile{}, listErr
|
||||
}
|
||||
var ownTeam *domain.Team
|
||||
for index := range teams {
|
||||
for _, slot := range teams[index].Slots {
|
||||
if slot.PlayerID == playerID {
|
||||
ownTeam = &teams[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ownTeam == nil {
|
||||
continue
|
||||
}
|
||||
series, listErr := s.Store.ListSeriesByEvent(ctx, event.ID)
|
||||
if listErr != nil {
|
||||
return domain.PlayerProfile{}, listErr
|
||||
}
|
||||
byID := make(map[string]domain.Team, len(teams))
|
||||
for _, team := range teams {
|
||||
byID[team.ID] = team
|
||||
}
|
||||
for _, item := range series {
|
||||
teamA, okA := byID[item.TeamAID]
|
||||
teamB, okB := byID[item.TeamBID]
|
||||
if okA && okB {
|
||||
matches = append(matches, domain.PlayerSeriesContext{Event: event, Series: item, TeamA: teamA, TeamB: teamB})
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain.BuildPlayerProfile(player, matches, 10), nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCommunitySettings(ctx context.Context) (domain.CommunitySettings, error) {
|
||||
return s.Store.GetCommunitySettings(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCommunitySettings(ctx context.Context, actor domain.Account, inviteURL string) (domain.CommunitySettings, error) {
|
||||
if !actor.IsAdmin() {
|
||||
return domain.CommunitySettings{}, domain.ErrForbidden
|
||||
}
|
||||
settings := domain.CommunitySettings{DiscordInviteURL: inviteURL, UpdatedAt: s.Now(), UpdatedBy: actor.ID}
|
||||
if err := settings.Validate(); err != nil {
|
||||
return domain.CommunitySettings{}, fmt.Errorf("%w: invalid Discord invite URL", err)
|
||||
}
|
||||
out, err := s.Store.UpdateCommunitySettings(ctx, settings)
|
||||
if err == nil {
|
||||
_ = s.Store.AppendAudit(ctx, actor.ID, "community.settings_updated", "community", out)
|
||||
s.Bus.Publish("community-settings", out)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) CreateEvent(ctx context.Context, actor domain.Account, event domain.Event, options CreateEventOptions) (domain.Event, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.Event{}, domain.ErrForbidden
|
||||
@@ -300,6 +388,9 @@ func (s *Service) CreateParticipant(ctx context.Context, actor domain.Account, e
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := player.ValidateIdentity(); err != nil {
|
||||
return domain.Player{}, domain.RSVP{}, err
|
||||
}
|
||||
rsvp := domain.RSVP{
|
||||
EventID: eventID,
|
||||
PlayerID: player.ID,
|
||||
|
||||
@@ -121,6 +121,85 @@ func TestCreateEventCanAnnounceWithoutEveryonePing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type communitySettingsStore struct {
|
||||
Store
|
||||
saved domain.CommunitySettings
|
||||
audits int
|
||||
}
|
||||
|
||||
func (s *communitySettingsStore) UpdateCommunitySettings(_ context.Context, settings domain.CommunitySettings) (domain.CommunitySettings, error) {
|
||||
s.saved = settings
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (s *communitySettingsStore) AppendAudit(_ context.Context, _, _, _ string, _ any) error {
|
||||
s.audits++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUpdateCommunitySettingsRequiresAdminAndAudits(t *testing.T) {
|
||||
store := &communitySettingsStore{}
|
||||
service := New(store, &recordingPublisher{}, nil)
|
||||
now := time.Date(2026, time.July, 19, 12, 0, 0, 0, time.UTC)
|
||||
service.Now = func() time.Time { return now }
|
||||
|
||||
if _, err := service.UpdateCommunitySettings(context.Background(), domain.Account{Role: domain.RoleModerator}, ""); !errors.Is(err, domain.ErrForbidden) {
|
||||
t.Fatalf("moderator must be forbidden, got %v", err)
|
||||
}
|
||||
out, err := service.UpdateCommunitySettings(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, " https://discord.gg/mixmaker ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.DiscordInviteURL != "https://discord.gg/mixmaker" || out.UpdatedBy != "admin" || !out.UpdatedAt.Equal(now) {
|
||||
t.Fatalf("unexpected settings: %+v", out)
|
||||
}
|
||||
if store.audits != 1 {
|
||||
t.Fatalf("expected one audit entry, got %d", store.audits)
|
||||
}
|
||||
}
|
||||
|
||||
type playerProfileStore struct {
|
||||
Store
|
||||
players []domain.Player
|
||||
saved domain.Player
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *playerProfileStore) ListPlayers(context.Context) ([]domain.Player, error) {
|
||||
return s.players, nil
|
||||
}
|
||||
|
||||
func (s *playerProfileStore) UpdatePlayer(_ context.Context, player domain.Player) (domain.Player, error) {
|
||||
s.saved = player
|
||||
return player, s.err
|
||||
}
|
||||
|
||||
func TestUpdateOwnProfileTrimsIdentityAndClearsBattleTag(t *testing.T) {
|
||||
store := &playerProfileStore{players: []domain.Player{{ID: "player"}}}
|
||||
service := New(store, &recordingPublisher{}, nil)
|
||||
name, battleTag := " New Name ", ""
|
||||
out, err := service.UpdateOwnProfile(context.Background(),
|
||||
domain.Account{ID: "account"},
|
||||
domain.Player{ID: "player", AccountID: "account", DisplayName: "Old", BattleTag: "Old#1234"},
|
||||
UpdateOwnProfileInput{
|
||||
DisplayName: &name, BattleTag: &battleTag,
|
||||
Ratings: domain.Ratings{Tank: 1, Damage: 20, Support: 40},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.DisplayName != "New Name" || out.BattleTag != "" {
|
||||
t.Fatalf("identity was not normalized and cleared: %+v", out)
|
||||
}
|
||||
if _, err = service.UpdateOwnProfile(context.Background(),
|
||||
domain.Account{ID: "other"}, domain.Player{AccountID: "account"},
|
||||
UpdateOwnProfileInput{},
|
||||
); !errors.Is(err, domain.ErrForbidden) {
|
||||
t.Fatalf("profile updates must remain self-only, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validEvent(now time.Time) domain.Event {
|
||||
return domain.Event{
|
||||
Name: "Sunday Mix",
|
||||
|
||||
Reference in New Issue
Block a user