xpt-shared-types 1.16.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/enums.d.ts +8 -0
- package/dist/generated/inputs.d.ts +26 -1
- package/dist/generated/models.d.ts +26 -1
- package/dist/manual/gameAccountHandle.d.ts +128 -0
- package/dist/manual/gameAccountHandle.js +221 -0
- package/dist/manual/index.d.ts +2 -0
- package/dist/manual/index.js +2 -0
- package/dist/manual/playerBan.d.ts +120 -0
- package/dist/manual/playerBan.js +45 -0
- package/dist/manual/tournamentStaff.d.ts +3 -1
- package/dist/manual/tournamentStaff.js +1 -0
- package/package.json +1 -1
- package/src/generated/enums.ts +31 -0
- package/src/generated/inputs.ts +31 -0
- package/src/generated/models.ts +31 -0
- package/src/manual/gameAccountHandle.ts +306 -0
- package/src/manual/index.ts +2 -0
- package/src/manual/playerBan.ts +123 -0
- package/src/manual/tournamentStaff.ts +4 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Player bans — the contract between `utils/playerBan.ts` in xpt-strapi and
|
|
4
|
+
* the ban dialog, ban list and host-bans page in xpt-client.
|
|
5
|
+
*
|
|
6
|
+
* Two scopes (`PlayerBanScope`, generated): **tournament** keeps a player out
|
|
7
|
+
* of one event and is set by its owner or an admin; **host** keeps them out
|
|
8
|
+
* of every tournament one host owns, now and later, and only that host sets
|
|
9
|
+
* it. The platform level is the account block, not a ban.
|
|
10
|
+
*
|
|
11
|
+
* A ban is active until it is lifted or its `expiresAt` passes; `null` is
|
|
12
|
+
* permanent. Lifted bans are kept, so a list can show history.
|
|
13
|
+
*
|
|
14
|
+
* Routes:
|
|
15
|
+
* - `GET|POST /tournaments/:id/bans`, `DELETE /tournaments/:id/bans/:userId`
|
|
16
|
+
* - `GET|POST /host-bans`, `DELETE /host-bans/:userId`
|
|
17
|
+
* Both GETs take `?includeLifted=true`.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.BAN_DURATIONS = exports.MAX_BAN_REASON_LENGTH = void 0;
|
|
21
|
+
exports.banExpiryFor = banExpiryFor;
|
|
22
|
+
exports.isBanActive = isBanActive;
|
|
23
|
+
exports.MAX_BAN_REASON_LENGTH = 500;
|
|
24
|
+
/** The durations the ban dialog offers. */
|
|
25
|
+
exports.BAN_DURATIONS = [
|
|
26
|
+
{ key: '1d', label: '24 hours', days: 1 },
|
|
27
|
+
{ key: '7d', label: '7 days', days: 7 },
|
|
28
|
+
{ key: '30d', label: '30 days', days: 30 },
|
|
29
|
+
{ key: 'permanent', label: 'Permanent', days: null },
|
|
30
|
+
];
|
|
31
|
+
/** `expiresAt` for a duration, or null for permanent. */
|
|
32
|
+
function banExpiryFor(key, now = new Date()) {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const days = (_b = (_a = exports.BAN_DURATIONS.find((d) => d.key === key)) === null || _a === void 0 ? void 0 : _a.days) !== null && _b !== void 0 ? _b : null;
|
|
35
|
+
return days == null
|
|
36
|
+
? null
|
|
37
|
+
: new Date(now.getTime() + days * 86400000).toISOString();
|
|
38
|
+
}
|
|
39
|
+
function isBanActive(ban, now = new Date()) {
|
|
40
|
+
if (ban.liftedAt)
|
|
41
|
+
return false;
|
|
42
|
+
if (!ban.expiresAt)
|
|
43
|
+
return true;
|
|
44
|
+
return new Date(ban.expiresAt).getTime() > now.getTime();
|
|
45
|
+
}
|
|
@@ -36,7 +36,9 @@ export type TournamentStaffAction =
|
|
|
36
36
|
/** Record scores, start matches, edit match details. */
|
|
37
37
|
| 'manageMatches'
|
|
38
38
|
/** Watch any lobby and post as staff in its chat. */
|
|
39
|
-
| 'enterLobby'
|
|
39
|
+
| 'enterLobby'
|
|
40
|
+
/** Ban players from this tournament and lift those bans. */
|
|
41
|
+
| 'manageBans';
|
|
40
42
|
export declare const TOURNAMENT_STAFF_CAPABILITIES: Record<TournamentStaffAction, readonly TournamentStaffRole[]>;
|
|
41
43
|
export declare const TOURNAMENT_STAFF_ROLE_LABELS: Record<TournamentStaffRole, string>;
|
|
42
44
|
export declare const tournamentStaffRoleLabel: (role: string | null | undefined) => string;
|
package/package.json
CHANGED
package/src/generated/enums.ts
CHANGED
|
@@ -22,6 +22,28 @@ export type FriendshipStatus =
|
|
|
22
22
|
| "pending"
|
|
23
23
|
| "accepted";
|
|
24
24
|
|
|
25
|
+
/** `GameAccount.code` */
|
|
26
|
+
export type GameAccountCode =
|
|
27
|
+
| "steam"
|
|
28
|
+
| "psn"
|
|
29
|
+
| "xbox"
|
|
30
|
+
| "battlenet"
|
|
31
|
+
| "riot"
|
|
32
|
+
| "epic"
|
|
33
|
+
| "ea"
|
|
34
|
+
| "activision"
|
|
35
|
+
| "ubisoft"
|
|
36
|
+
| "nintendo"
|
|
37
|
+
| "fightcade"
|
|
38
|
+
| "apple"
|
|
39
|
+
| "googleplay";
|
|
40
|
+
|
|
41
|
+
/** `GameAccount.verification` */
|
|
42
|
+
export type GameAccountVerification =
|
|
43
|
+
| "none"
|
|
44
|
+
| "format"
|
|
45
|
+
| "linked";
|
|
46
|
+
|
|
25
47
|
/** `Game.genre` */
|
|
26
48
|
export type GameGenre =
|
|
27
49
|
| "FPS"
|
|
@@ -126,6 +148,11 @@ export type PlatformCode =
|
|
|
126
148
|
| "nintendo"
|
|
127
149
|
| "mobile";
|
|
128
150
|
|
|
151
|
+
/** `PlayerBan.scope` */
|
|
152
|
+
export type PlayerBanScope =
|
|
153
|
+
| "tournament"
|
|
154
|
+
| "host";
|
|
155
|
+
|
|
129
156
|
/** `Referral.status` */
|
|
130
157
|
export type ReferralStatus =
|
|
131
158
|
| "pending"
|
|
@@ -228,6 +255,10 @@ export type TournamentVisibility =
|
|
|
228
255
|
| "public"
|
|
229
256
|
| "unlisted";
|
|
230
257
|
|
|
258
|
+
/** `UserGameAccount.verificationMethod` */
|
|
259
|
+
export type UserGameAccountVerificationMethod =
|
|
260
|
+
| "linked";
|
|
261
|
+
|
|
231
262
|
/** `UserTransaction.stripeStatus` */
|
|
232
263
|
export type UserTransactionStripeStatus =
|
|
233
264
|
| "pending"
|
package/src/generated/inputs.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
AdminActionTargetType,
|
|
12
12
|
FaqCategory,
|
|
13
13
|
FriendshipStatus,
|
|
14
|
+
GameAccountCode,
|
|
15
|
+
GameAccountVerification,
|
|
14
16
|
GameGenre,
|
|
15
17
|
GameRequestCustomLobbies,
|
|
16
18
|
GameRequestGenre,
|
|
@@ -23,6 +25,7 @@ import type {
|
|
|
23
25
|
MatchStreamPlatform,
|
|
24
26
|
MatchWinnerSlot,
|
|
25
27
|
PlatformCode,
|
|
28
|
+
PlayerBanScope,
|
|
26
29
|
ReferralStatus,
|
|
27
30
|
TeamCurrentStatus,
|
|
28
31
|
TeamInviteStatus,
|
|
@@ -39,6 +42,7 @@ import type {
|
|
|
39
42
|
TournamentTeamSize,
|
|
40
43
|
TournamentType,
|
|
41
44
|
TournamentVisibility,
|
|
45
|
+
UserGameAccountVerificationMethod,
|
|
42
46
|
UserTransactionStripeStatus,
|
|
43
47
|
UserTransactionType,
|
|
44
48
|
} from './enums';
|
|
@@ -126,6 +130,8 @@ export interface GameInput {
|
|
|
126
130
|
/** Write payload for `game-account`. */
|
|
127
131
|
export interface GameAccountInput {
|
|
128
132
|
name?: string;
|
|
133
|
+
code?: GameAccountCode;
|
|
134
|
+
verification?: GameAccountVerification;
|
|
129
135
|
tournaments?: RelationInput | RelationInput[];
|
|
130
136
|
user_game_accounts?: RelationInput | RelationInput[];
|
|
131
137
|
imgThumb?: MediaInput;
|
|
@@ -133,6 +139,15 @@ export interface GameAccountInput {
|
|
|
133
139
|
platforms?: RelationInput | RelationInput[];
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
/** Write payload for `game-account-link`. */
|
|
143
|
+
export interface GameAccountLinkInput {
|
|
144
|
+
token?: string;
|
|
145
|
+
users_permissions_user?: RelationInput;
|
|
146
|
+
game_account?: RelationInput;
|
|
147
|
+
expiresAt?: string;
|
|
148
|
+
returnTo?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
/** Write payload for `game-request`. */
|
|
137
152
|
export interface GameRequestInput {
|
|
138
153
|
gameTitle?: string;
|
|
@@ -247,6 +262,19 @@ export interface PlatformInput {
|
|
|
247
262
|
game_accounts?: RelationInput | RelationInput[];
|
|
248
263
|
}
|
|
249
264
|
|
|
265
|
+
/** Write payload for `player-ban`. */
|
|
266
|
+
export interface PlayerBanInput {
|
|
267
|
+
scope?: PlayerBanScope;
|
|
268
|
+
tournament?: RelationInput;
|
|
269
|
+
host?: RelationInput;
|
|
270
|
+
users_permissions_user?: RelationInput;
|
|
271
|
+
bannedBy?: RelationInput;
|
|
272
|
+
reason?: string;
|
|
273
|
+
expiresAt?: string;
|
|
274
|
+
liftedAt?: string;
|
|
275
|
+
liftedBy?: RelationInput;
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
/** Write payload for `preset-avatar`. */
|
|
251
279
|
export interface PresetAvatarInput {
|
|
252
280
|
image?: MediaInput;
|
|
@@ -494,6 +522,9 @@ export interface UserInput {
|
|
|
494
522
|
export interface UserGameAccountInput {
|
|
495
523
|
accountId?: string;
|
|
496
524
|
isPrivate?: boolean;
|
|
525
|
+
verifiedAt?: string;
|
|
526
|
+
verificationMethod?: UserGameAccountVerificationMethod;
|
|
527
|
+
providerAccountId?: string;
|
|
497
528
|
users_permissions_user?: RelationInput;
|
|
498
529
|
game_account?: RelationInput;
|
|
499
530
|
}
|
package/src/generated/models.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
AdminActionTargetType,
|
|
13
13
|
FaqCategory,
|
|
14
14
|
FriendshipStatus,
|
|
15
|
+
GameAccountCode,
|
|
16
|
+
GameAccountVerification,
|
|
15
17
|
GameGenre,
|
|
16
18
|
GameRequestCustomLobbies,
|
|
17
19
|
GameRequestGenre,
|
|
@@ -24,6 +26,7 @@ import type {
|
|
|
24
26
|
MatchStreamPlatform,
|
|
25
27
|
MatchWinnerSlot,
|
|
26
28
|
PlatformCode,
|
|
29
|
+
PlayerBanScope,
|
|
27
30
|
ReferralStatus,
|
|
28
31
|
TeamCurrentStatus,
|
|
29
32
|
TeamInviteStatus,
|
|
@@ -40,6 +43,7 @@ import type {
|
|
|
40
43
|
TournamentTeamSize,
|
|
41
44
|
TournamentType,
|
|
42
45
|
TournamentVisibility,
|
|
46
|
+
UserGameAccountVerificationMethod,
|
|
43
47
|
UserTransactionStripeStatus,
|
|
44
48
|
UserTransactionType,
|
|
45
49
|
} from './enums';
|
|
@@ -127,6 +131,8 @@ export interface Game extends StrapiDocument {
|
|
|
127
131
|
/** `game-account` */
|
|
128
132
|
export interface GameAccount extends StrapiDocument {
|
|
129
133
|
name?: string | null;
|
|
134
|
+
code?: GameAccountCode | null;
|
|
135
|
+
verification?: GameAccountVerification | null;
|
|
130
136
|
tournaments?: Tournament[];
|
|
131
137
|
user_game_accounts?: UserGameAccount[];
|
|
132
138
|
imgThumb?: StrapiMedia | null;
|
|
@@ -134,6 +140,15 @@ export interface GameAccount extends StrapiDocument {
|
|
|
134
140
|
platforms?: Platform[];
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
/** `game-account-link` */
|
|
144
|
+
export interface GameAccountLink extends StrapiDocument {
|
|
145
|
+
token?: string | null;
|
|
146
|
+
users_permissions_user?: User | null;
|
|
147
|
+
game_account?: GameAccount | null;
|
|
148
|
+
expiresAt?: string | null;
|
|
149
|
+
returnTo?: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
/** `game-request` */
|
|
138
153
|
export interface GameRequest extends StrapiDocument {
|
|
139
154
|
gameTitle?: string | null;
|
|
@@ -248,6 +263,19 @@ export interface Platform extends StrapiDocument {
|
|
|
248
263
|
game_accounts?: GameAccount[];
|
|
249
264
|
}
|
|
250
265
|
|
|
266
|
+
/** `player-ban` */
|
|
267
|
+
export interface PlayerBan extends StrapiDocument {
|
|
268
|
+
scope?: PlayerBanScope | null;
|
|
269
|
+
tournament?: Tournament | null;
|
|
270
|
+
host?: User | null;
|
|
271
|
+
users_permissions_user?: User | null;
|
|
272
|
+
bannedBy?: User | null;
|
|
273
|
+
reason?: string | null;
|
|
274
|
+
expiresAt?: string | null;
|
|
275
|
+
liftedAt?: string | null;
|
|
276
|
+
liftedBy?: User | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
251
279
|
/** `preset-avatar` */
|
|
252
280
|
export interface PresetAvatar extends StrapiDocument {
|
|
253
281
|
image?: StrapiMedia | null;
|
|
@@ -495,6 +523,9 @@ export interface User extends StrapiDocument {
|
|
|
495
523
|
export interface UserGameAccount extends StrapiDocument {
|
|
496
524
|
accountId?: string | null;
|
|
497
525
|
isPrivate?: boolean | null;
|
|
526
|
+
verifiedAt?: string | null;
|
|
527
|
+
verificationMethod?: UserGameAccountVerificationMethod | null;
|
|
528
|
+
providerAccountId?: string | null;
|
|
498
529
|
users_permissions_user?: User | null;
|
|
499
530
|
game_account?: GameAccount | null;
|
|
500
531
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a player's handle on each account network looks like.
|
|
3
|
+
*
|
|
4
|
+
* A handle is the one thing opponents actually exchange — the PSN ID, the
|
|
5
|
+
* BattleTag, the Riot ID — so a typo in it is a match that cannot start. Until
|
|
6
|
+
* these rules existed one regex covered every network, and it happily accepted
|
|
7
|
+
* `Name#1234` for PSN (where `#` is not allowed) while rejecting nothing that a
|
|
8
|
+
* real PSN or Xbox rule would.
|
|
9
|
+
*
|
|
10
|
+
* None of this proves ownership. A network whose handles can be linked by
|
|
11
|
+
* OAuth (Steam, Battle.net, Xbox) gets a `linked` tier later; this file is the
|
|
12
|
+
* `format` tier that every network can have today. Which tier applies is a
|
|
13
|
+
* property of the catalogue row (`GameAccount.verification`), set by editors,
|
|
14
|
+
* so a rule that turns out wrong for some network can be switched off without a
|
|
15
|
+
* deploy: set the row to `none` and the generic rule applies.
|
|
16
|
+
*
|
|
17
|
+
* Lives here because the settings page validates as the player types and the
|
|
18
|
+
* backend validates again on write. Two copies of a regex is two chances for a
|
|
19
|
+
* handle to pass one and fail the other.
|
|
20
|
+
*
|
|
21
|
+
* Pure on purpose: no I/O, no profanity list (each app keeps its own), so the
|
|
22
|
+
* rules are unit-tested directly (xpt-strapi
|
|
23
|
+
* `tests/unit/utils/gameAccountHandle.test.ts`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
GameAccountCode,
|
|
28
|
+
GameAccountVerification,
|
|
29
|
+
} from '../generated/enums';
|
|
30
|
+
|
|
31
|
+
export interface HandleRule {
|
|
32
|
+
/** The network this rule is for; absent on the generic fallback. */
|
|
33
|
+
code?: GameAccountCode;
|
|
34
|
+
/** What the field is called in copy: "PSN online ID", "BattleTag". */
|
|
35
|
+
label: string;
|
|
36
|
+
/** The field's placeholder. */
|
|
37
|
+
placeholder: string;
|
|
38
|
+
/** One sentence under the field saying what a valid value looks like. */
|
|
39
|
+
hint: string;
|
|
40
|
+
/** Applied to the normalized value. */
|
|
41
|
+
pattern: RegExp;
|
|
42
|
+
min: number;
|
|
43
|
+
max: number;
|
|
44
|
+
/**
|
|
45
|
+
* Rewrites the trimmed input before `pattern` is applied: a Steam profile
|
|
46
|
+
* URL down to its id, a bare Nintendo friend code into `SW-xxxx-xxxx-xxxx`.
|
|
47
|
+
* The result is what gets stored.
|
|
48
|
+
*/
|
|
49
|
+
normalize?: (value: string) => string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type HandleCheck =
|
|
53
|
+
| { ok: true; value: string }
|
|
54
|
+
| { ok: false; message: string };
|
|
55
|
+
|
|
56
|
+
/** `error.details.name` values the game-account write endpoints raise. */
|
|
57
|
+
export type GameAccountErrorName =
|
|
58
|
+
| 'HandleInvalid'
|
|
59
|
+
| 'HandleTaken'
|
|
60
|
+
/** The row is verified by a link; unlink before changing the ID. */
|
|
61
|
+
| 'HandleLocked'
|
|
62
|
+
/** No configured link provider for this network. */
|
|
63
|
+
| 'LinkUnavailable'
|
|
64
|
+
/** The provider account is already linked to another XPT user. */
|
|
65
|
+
| 'LinkConflict';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Networks whose handles can be verified by signing in to the network. The
|
|
69
|
+
* catalogue's `verification: 'linked'` is only valid for these; whether a
|
|
70
|
+
* provider is actually configured on a given environment is answered at
|
|
71
|
+
* runtime by `GET /user-game-accounts/link/providers`.
|
|
72
|
+
*/
|
|
73
|
+
export const LINK_PROVIDER_CODES = ['steam', 'battlenet'] as const satisfies
|
|
74
|
+
readonly GameAccountCode[];
|
|
75
|
+
|
|
76
|
+
export type LinkProviderCode = (typeof LINK_PROVIDER_CODES)[number];
|
|
77
|
+
|
|
78
|
+
export function isLinkableCode(
|
|
79
|
+
code: string | null | undefined
|
|
80
|
+
): code is LinkProviderCode {
|
|
81
|
+
return (LINK_PROVIDER_CODES as readonly string[]).includes(code ?? '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Why a link attempt came back without a verified account — the `linkError`
|
|
86
|
+
* query value the callback redirects to the settings page with.
|
|
87
|
+
*/
|
|
88
|
+
export type AccountLinkError =
|
|
89
|
+
/** The attempt was unknown, already used, or older than its window. */
|
|
90
|
+
| 'expired'
|
|
91
|
+
/** The player cancelled at the provider. */
|
|
92
|
+
| 'denied'
|
|
93
|
+
/** The provider's answer did not verify. */
|
|
94
|
+
| 'invalid'
|
|
95
|
+
/** That provider account is already linked to another XPT user. */
|
|
96
|
+
| 'taken'
|
|
97
|
+
/** The provider is not configured, or the network is not linkable. */
|
|
98
|
+
| 'unavailable'
|
|
99
|
+
/** Accounts are frozen while the player is in a running tournament. */
|
|
100
|
+
| 'activeTournament';
|
|
101
|
+
|
|
102
|
+
/** True when a row's handle was proved by signing in to the network. */
|
|
103
|
+
export function isVerifiedAccount(
|
|
104
|
+
row:
|
|
105
|
+
| { verifiedAt?: string | Date | null; verificationMethod?: string | null }
|
|
106
|
+
| null
|
|
107
|
+
| undefined
|
|
108
|
+
): boolean {
|
|
109
|
+
return !!row?.verifiedAt && row.verificationMethod === 'linked';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The rule every network gets when it has no stricter one. Also the backstop
|
|
114
|
+
* the backend applies to every write regardless of network, so every rule
|
|
115
|
+
* below must produce a value that passes this one.
|
|
116
|
+
*
|
|
117
|
+
* Letters are `\p{L}` rather than `A-Z`: BattleTags and Riot IDs allow accented
|
|
118
|
+
* names, and a rule that rejected "Ærøn#1234" would be wrong, not strict.
|
|
119
|
+
*/
|
|
120
|
+
export const GENERIC_HANDLE_RULE: HandleRule = {
|
|
121
|
+
label: 'Game ID',
|
|
122
|
+
placeholder: 'Game ID',
|
|
123
|
+
hint: 'Enter your ID exactly as it appears in-game. Letters, numbers, spaces and _ . # - are allowed.',
|
|
124
|
+
pattern: /^[\p{L}\p{N}_ .#-]+$/u,
|
|
125
|
+
min: 3,
|
|
126
|
+
max: 32,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const STEAM_PROFILE_URL =
|
|
130
|
+
/^(?:https?:\/\/)?(?:www\.)?steamcommunity\.com\/(?:id|profiles)\/([^/?#\s]+)\/?(?:[?#].*)?$/i;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Per-network rules. Initial values, and product decisions rather than facts:
|
|
134
|
+
* each is the documented shape as of writing, kept a little looser than the
|
|
135
|
+
* network's own signup validator so a real handle is never refused.
|
|
136
|
+
*/
|
|
137
|
+
export const HANDLE_RULES: Partial<Record<GameAccountCode, HandleRule>> = {
|
|
138
|
+
psn: {
|
|
139
|
+
code: 'psn',
|
|
140
|
+
label: 'PSN online ID',
|
|
141
|
+
placeholder: 'PSN online ID',
|
|
142
|
+
hint: '3 to 16 letters, numbers, hyphens or underscores, starting with a letter. No # tag.',
|
|
143
|
+
pattern: /^[A-Za-z][A-Za-z0-9_-]{2,15}$/,
|
|
144
|
+
min: 3,
|
|
145
|
+
max: 16,
|
|
146
|
+
},
|
|
147
|
+
xbox: {
|
|
148
|
+
code: 'xbox',
|
|
149
|
+
label: 'Gamertag',
|
|
150
|
+
placeholder: 'Gamertag or Gamertag#1234',
|
|
151
|
+
hint: 'Letters, numbers and single spaces, with the #1234 suffix if your gamertag has one.',
|
|
152
|
+
pattern: /^[A-Za-z0-9]+(?: [A-Za-z0-9]+)*(?:#\d{3,4})?$/,
|
|
153
|
+
min: 3,
|
|
154
|
+
max: 20,
|
|
155
|
+
},
|
|
156
|
+
battlenet: {
|
|
157
|
+
code: 'battlenet',
|
|
158
|
+
label: 'BattleTag',
|
|
159
|
+
placeholder: 'Name#1234',
|
|
160
|
+
hint: 'Your BattleTag including the number, e.g. Name#1234.',
|
|
161
|
+
pattern: /^\p{L}[\p{L}\p{N}]{2,11}#\d{4,7}$/u,
|
|
162
|
+
min: 8,
|
|
163
|
+
max: 20,
|
|
164
|
+
},
|
|
165
|
+
riot: {
|
|
166
|
+
code: 'riot',
|
|
167
|
+
label: 'Riot ID',
|
|
168
|
+
placeholder: 'GameName#TAG',
|
|
169
|
+
hint: 'Your Riot ID including the tagline, e.g. GameName#TAG.',
|
|
170
|
+
pattern: /^[\p{L}\p{N}](?:[\p{L}\p{N} ]{1,14}[\p{L}\p{N}])?#[\p{L}\p{N}]{3,5}$/u,
|
|
171
|
+
min: 7,
|
|
172
|
+
max: 22,
|
|
173
|
+
},
|
|
174
|
+
epic: {
|
|
175
|
+
code: 'epic',
|
|
176
|
+
label: 'Epic display name',
|
|
177
|
+
placeholder: 'Epic display name',
|
|
178
|
+
hint: '3 to 16 letters, numbers, spaces or - _ . as shown on your Epic account.',
|
|
179
|
+
pattern: /^[\p{L}\p{N}][\p{L}\p{N} ._-]{1,14}[\p{L}\p{N}._-]$/u,
|
|
180
|
+
min: 3,
|
|
181
|
+
max: 16,
|
|
182
|
+
},
|
|
183
|
+
ea: {
|
|
184
|
+
code: 'ea',
|
|
185
|
+
label: 'EA ID',
|
|
186
|
+
placeholder: 'EA ID',
|
|
187
|
+
hint: '4 to 16 letters, numbers, hyphens or underscores.',
|
|
188
|
+
pattern: /^[A-Za-z0-9_-]{4,16}$/,
|
|
189
|
+
min: 4,
|
|
190
|
+
max: 16,
|
|
191
|
+
},
|
|
192
|
+
activision: {
|
|
193
|
+
code: 'activision',
|
|
194
|
+
label: 'Activision ID',
|
|
195
|
+
placeholder: 'Name#1234567',
|
|
196
|
+
hint: 'Your Activision ID including the number, e.g. Name#1234567.',
|
|
197
|
+
pattern: /^[A-Za-z0-9 _-]{2,16}#\d{4,8}$/,
|
|
198
|
+
min: 7,
|
|
199
|
+
max: 25,
|
|
200
|
+
},
|
|
201
|
+
ubisoft: {
|
|
202
|
+
code: 'ubisoft',
|
|
203
|
+
label: 'Ubisoft username',
|
|
204
|
+
placeholder: 'Ubisoft username',
|
|
205
|
+
hint: '3 to 15 letters, numbers or - _ . as shown in Ubisoft Connect.',
|
|
206
|
+
pattern: /^[A-Za-z0-9._-]{3,15}$/,
|
|
207
|
+
min: 3,
|
|
208
|
+
max: 15,
|
|
209
|
+
},
|
|
210
|
+
steam: {
|
|
211
|
+
code: 'steam',
|
|
212
|
+
label: 'Steam ID',
|
|
213
|
+
placeholder: 'Friend code or profile URL',
|
|
214
|
+
hint: 'Your Steam friend code (Friends > Add a Friend), your SteamID64, or your steamcommunity.com profile URL.',
|
|
215
|
+
// A profile URL is the easiest thing for a player to paste; what gets
|
|
216
|
+
// stored is the id or custom name at the end of it.
|
|
217
|
+
normalize: (value) => value.replace(STEAM_PROFILE_URL, '$1'),
|
|
218
|
+
pattern: /^[A-Za-z0-9_-]{3,32}$/,
|
|
219
|
+
min: 3,
|
|
220
|
+
max: 32,
|
|
221
|
+
},
|
|
222
|
+
nintendo: {
|
|
223
|
+
code: 'nintendo',
|
|
224
|
+
label: 'Nintendo friend code',
|
|
225
|
+
placeholder: 'SW-0000-0000-0000',
|
|
226
|
+
hint: 'Your 12-digit Switch friend code, e.g. SW-1234-5678-9012.',
|
|
227
|
+
normalize: (value) => {
|
|
228
|
+
const digits = value.replace(/^sw/i, '').replace(/[\s-]/g, '');
|
|
229
|
+
return /^\d{12}$/.test(digits)
|
|
230
|
+
? `SW-${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8)}`
|
|
231
|
+
: value;
|
|
232
|
+
},
|
|
233
|
+
pattern: /^SW-\d{4}-\d{4}-\d{4}$/,
|
|
234
|
+
min: 17,
|
|
235
|
+
max: 17,
|
|
236
|
+
},
|
|
237
|
+
// fightcade, apple and googleplay have no documented handle shape and stay
|
|
238
|
+
// on the generic rule.
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The rule a network's handles are checked against: its own when the
|
|
243
|
+
* catalogue row asks for format checks (or is linkable, which implies them
|
|
244
|
+
* for anything typed by hand) and a rule exists for its code, otherwise the
|
|
245
|
+
* generic one.
|
|
246
|
+
*/
|
|
247
|
+
export function handleRuleFor(
|
|
248
|
+
network:
|
|
249
|
+
| {
|
|
250
|
+
code?: GameAccountCode | string | null;
|
|
251
|
+
verification?: GameAccountVerification | string | null;
|
|
252
|
+
}
|
|
253
|
+
| null
|
|
254
|
+
| undefined
|
|
255
|
+
): HandleRule {
|
|
256
|
+
const checked =
|
|
257
|
+
network?.verification === 'format' || network?.verification === 'linked';
|
|
258
|
+
if (!checked || !network?.code) {
|
|
259
|
+
return GENERIC_HANDLE_RULE;
|
|
260
|
+
}
|
|
261
|
+
return HANDLE_RULES[network.code as GameAccountCode] ?? GENERIC_HANDLE_RULE;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** True when the code has a rule of its own, so `format` means something. */
|
|
265
|
+
export function hasHandleRule(code: string | null | undefined): boolean {
|
|
266
|
+
return !!code && Object.prototype.hasOwnProperty.call(HANDLE_RULES, code);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks `raw` against `rule` and returns the value to store: trimmed, runs of
|
|
271
|
+
* whitespace collapsed to one space, then the rule's own normalization.
|
|
272
|
+
*
|
|
273
|
+
* The message is the one the player reads, so it says what a valid value looks
|
|
274
|
+
* like rather than which check failed.
|
|
275
|
+
*/
|
|
276
|
+
export function checkHandle(
|
|
277
|
+
raw: string | null | undefined,
|
|
278
|
+
rule: HandleRule
|
|
279
|
+
): HandleCheck {
|
|
280
|
+
const collapsed = (raw ?? '').trim().replace(/\s+/g, ' ');
|
|
281
|
+
if (!collapsed) {
|
|
282
|
+
return { ok: false, message: `Please enter your ${rule.label}` };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const value = rule.normalize ? rule.normalize(collapsed) : collapsed;
|
|
286
|
+
|
|
287
|
+
if (value.length < rule.min) {
|
|
288
|
+
return { ok: false, message: `${rule.label} is too short. ${rule.hint}` };
|
|
289
|
+
}
|
|
290
|
+
if (value.length > rule.max) {
|
|
291
|
+
return { ok: false, message: `${rule.label} is too long. ${rule.hint}` };
|
|
292
|
+
}
|
|
293
|
+
if (!rule.pattern.test(value)) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
message: `That doesn't look like a valid ${rule.label}. ${rule.hint}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ok: true, value };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The key two handles are compared on: same network, same letters, any case. */
|
|
304
|
+
export function normalizeHandleKey(value: string): string {
|
|
305
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
306
|
+
}
|
package/src/manual/index.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Hand-written types that are not derived from a Strapi content type.
|
|
3
3
|
*/
|
|
4
4
|
export * from './country';
|
|
5
|
+
export * from './gameAccountHandle';
|
|
5
6
|
export * from './lobby';
|
|
6
7
|
export * from './notification';
|
|
8
|
+
export * from './playerBan';
|
|
7
9
|
export * from './tournamentEntry';
|
|
8
10
|
export * from './tournamentInvite';
|
|
9
11
|
export * from './tournamentJoinRequest';
|