feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+104 -18
View File
@@ -35,11 +35,10 @@ type GetUserByEmailInput struct {
Email string
}
// GetUserByRaceNameInput stores one exact trusted lookup by exact stored race
// name.
type GetUserByRaceNameInput struct {
// RaceName stores the exact current race name to resolve.
RaceName string
// GetUserByUserNameInput stores one exact trusted lookup by stored user name.
type GetUserByUserNameInput struct {
// UserName stores the exact `player-<suffix>` handle to resolve.
UserName string
}
// ListUsersInput stores one trusted administrative user-list request.
@@ -71,6 +70,16 @@ type ListUsersInput struct {
// LimitCode stores the optional active user-specific limit filter.
LimitCode string
// UserName stores the optional exact `user_name` filter.
UserName string
// DisplayName stores the optional `display_name` filter value.
DisplayName string
// DisplayNameMatch selects between `exact` (default) and `prefix` matching
// for DisplayName. An empty value is treated as `exact`.
DisplayNameMatch string
// CanLogin stores the optional derived login-eligibility filter.
CanLogin *bool
@@ -207,40 +216,39 @@ func (service *ByEmailGetter) Execute(ctx context.Context, input GetUserByEmailI
return LookupResult{User: aggregate.View()}, nil
}
// ByRaceNameGetter executes exact trusted lookups by exact stored race name.
type ByRaceNameGetter struct {
// ByUserNameGetter executes exact trusted lookups by stored user name.
type ByUserNameGetter struct {
support readSupport
}
// NewByRaceNameGetter constructs one exact admin lookup by exact stored race
// name.
func NewByRaceNameGetter(
// NewByUserNameGetter constructs one exact admin lookup by stored user name.
func NewByUserNameGetter(
accounts ports.UserAccountStore,
entitlements entitlementReader,
sanctions ports.SanctionStore,
limits ports.LimitStore,
clock ports.Clock,
) (*ByRaceNameGetter, error) {
) (*ByUserNameGetter, error) {
support, err := newReadSupport(accounts, entitlements, sanctions, limits, clock)
if err != nil {
return nil, fmt.Errorf("admin users by-race-name getter: %w", err)
return nil, fmt.Errorf("admin users by-user-name getter: %w", err)
}
return &ByRaceNameGetter{support: support}, nil
return &ByUserNameGetter{support: support}, nil
}
// Execute resolves one exact user by exact stored race name.
func (service *ByRaceNameGetter) Execute(ctx context.Context, input GetUserByRaceNameInput) (LookupResult, error) {
// Execute resolves one exact user by stored user name.
func (service *ByUserNameGetter) Execute(ctx context.Context, input GetUserByUserNameInput) (LookupResult, error) {
if ctx == nil {
return LookupResult{}, shared.InvalidRequest("context must not be nil")
}
raceName, err := shared.ParseRaceName(input.RaceName)
userName, err := shared.ParseUserName(input.UserName)
if err != nil {
return LookupResult{}, err
}
record, err := service.support.accounts.GetByRaceName(ctx, raceName)
record, err := service.support.accounts.GetByUserName(ctx, userName)
switch {
case err == nil:
case errors.Is(err, ports.ErrNotFound):
@@ -333,7 +341,19 @@ func (service *Lister) Execute(ctx context.Context, input ListUsersInput) (ListU
candidateID := candidatePage.UserIDs[0]
aggregate, err := service.support.loader.Load(ctx, candidateID)
if err != nil {
switch {
case err == nil:
case shared.CodeOf(err) == shared.ErrorCodeSubjectNotFound:
// Soft-deleted accounts are silently skipped from the default admin
// listing per Stage 22. The candidate index may still reference them
// while their account record carries a DeletedAt timestamp.
if nextToken == "" {
result.NextPageToken = ""
return result, nil
}
currentToken = nextToken
continue
default:
return ListUsersResult{}, err
}
if matchesFilters(aggregate, filters) {
@@ -382,6 +402,18 @@ func parseListFilters(input ListUsersInput) (ports.UserListFilters, error) {
if err != nil {
return ports.UserListFilters{}, err
}
userName, err := parseListUserName(input.UserName)
if err != nil {
return ports.UserListFilters{}, err
}
displayName, err := parseListDisplayName(input.DisplayName)
if err != nil {
return ports.UserListFilters{}, err
}
displayNameMatch, err := parseListDisplayNameMatch(input.DisplayNameMatch, displayName)
if err != nil {
return ports.UserListFilters{}, err
}
filters := ports.UserListFilters{
PaidState: paidState,
@@ -390,6 +422,9 @@ func parseListFilters(input ListUsersInput) (ports.UserListFilters, error) {
DeclaredCountry: declaredCountry,
SanctionCode: sanctionCode,
LimitCode: limitCode,
UserName: userName,
DisplayName: displayName,
DisplayNameMatch: displayNameMatch,
CanLogin: input.CanLogin,
CanCreatePrivateGame: input.CanCreatePrivateGame,
CanJoinGame: input.CanJoinGame,
@@ -401,6 +436,40 @@ func parseListFilters(input ListUsersInput) (ports.UserListFilters, error) {
return filters, nil
}
func parseListUserName(value string) (common.UserName, error) {
trimmed := shared.NormalizeString(value)
if trimmed == "" {
return "", nil
}
return shared.ParseUserName(trimmed)
}
func parseListDisplayName(value string) (common.DisplayName, error) {
trimmed := shared.NormalizeString(value)
if trimmed == "" {
return "", nil
}
return shared.ParseDisplayName(trimmed)
}
func parseListDisplayNameMatch(value string, displayName common.DisplayName) (ports.DisplayNameMatchMode, error) {
trimmed := shared.NormalizeString(value)
if trimmed == "" {
return "", nil
}
mode := ports.DisplayNameMatchMode(trimmed)
if !mode.IsKnown() {
return "", shared.InvalidRequest(fmt.Sprintf("display_name_match %q is unsupported", trimmed))
}
if displayName.IsZero() {
return "", shared.InvalidRequest("display_name_match requires display_name")
}
return mode, nil
}
func parsePaidState(value string) (entitlement.PaidState, error) {
state := entitlement.PaidState(shared.NormalizeString(value))
if !state.IsKnown() {
@@ -477,6 +546,23 @@ func matchesFilters(aggregate accountview.Aggregate, filters ports.UserListFilte
if filters.LimitCode != "" && !aggregate.HasActiveLimit(filters.LimitCode) {
return false
}
if !filters.UserName.IsZero() && aggregate.AccountRecord.UserName != filters.UserName {
return false
}
if !filters.DisplayName.IsZero() {
recordDisplayName := aggregate.AccountRecord.DisplayName.String()
filterValue := filters.DisplayName.String()
switch filters.DisplayNameMatch {
case ports.DisplayNameMatchModePrefix:
if !strings.HasPrefix(recordDisplayName, filterValue) {
return false
}
default:
if recordDisplayName != filterValue {
return false
}
}
}
canLogin, canCreatePrivateGame, canJoinGame := deriveFilterEligibility(aggregate)
if filters.CanLogin != nil && canLogin != *filters.CanLogin {