@diffenderfer-games/hub-contract 2.0.0-next.0 → 2.0.0-next.2
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/CHANGELOG.md +24 -0
- package/LICENSE +21 -21
- package/README.md +26 -26
- package/dist/index.d.ts +2683 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +862 -18
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
- package/src/api/analytics.ts +81 -81
- package/src/api/auth.ts +77 -77
- package/src/api/catalog-activity.ts +66 -0
- package/src/api/catalog.ts +177 -171
- package/src/api/daily.ts +122 -122
- package/src/api/economy/bundle.ts +69 -69
- package/src/api/index.ts +92 -85
- package/src/api/leaderboards.ts +151 -151
- package/src/api/messages.ts +142 -142
- package/src/api/mp/chat.ts +36 -36
- package/src/api/mp/invite-links.ts +42 -0
- package/src/api/mp/parties.ts +5 -0
- package/src/api/mp/races.ts +211 -211
- package/src/api/mp/rooms.ts +122 -90
- package/src/api/mp/snapshot.ts +17 -17
- package/src/api/mp.ts +135 -110
- package/src/api/notify.ts +114 -111
- package/src/api/offline.ts +45 -0
- package/src/api/progress/game-data.ts +189 -189
- package/src/api/progress.ts +59 -59
- package/src/api/race.ts +47 -47
- package/src/api/reports.ts +208 -208
- package/src/api/social/config.ts +47 -47
- package/src/api/social/endpoints.ts +179 -179
- package/src/api/social/people.ts +118 -118
- package/src/api/social/presence.ts +95 -95
- package/src/api/social.ts +9 -9
- package/src/api/tournaments/endpoints.ts +102 -0
- package/src/api/tournaments/play-online.ts +71 -0
- package/src/api/tournaments/shapes.ts +202 -0
- package/src/api/tournaments.ts +7 -0
- package/src/api/version.ts +40 -40
- package/src/coerce.ts +50 -50
- package/src/common.ts +42 -42
- package/src/entities.ts +62 -62
- package/src/errors/codes/core.ts +10 -10
- package/src/errors/codes/multiplayer.ts +56 -43
- package/src/errors/codes/play.ts +5 -5
- package/src/errors/codes/social.ts +31 -31
- package/src/errors/index.ts +88 -88
- package/src/index.ts +16 -16
- package/src/limits.ts +125 -100
- package/src/logic/daily.ts +54 -54
- package/src/logic/index.ts +8 -8
- package/src/logic/presence.ts +62 -62
- package/src/logic/time.ts +81 -81
- package/src/manifest/changes.ts +55 -55
- package/src/manifest/controls.ts +42 -42
- package/src/manifest/fields.ts +89 -89
- package/src/manifest/game.ts +139 -132
- package/src/manifest/index.ts +13 -12
- package/src/manifest/inject.ts +31 -31
- package/src/manifest/labels.ts +99 -0
- package/src/manifest/leaderboards.ts +29 -29
- package/src/manifest/multiplayer.ts +79 -71
- package/src/manifest/race.ts +93 -93
- package/src/middleware.ts +47 -47
- package/src/realtime/define.ts +52 -52
- package/src/realtime/envelope.ts +67 -67
- package/src/realtime/events/multiplayer.ts +9 -8
- package/src/realtime/events/parties.ts +31 -31
- package/src/realtime/events/races.ts +27 -27
- package/src/realtime/events/rooms.ts +52 -48
- package/src/realtime/events/social.ts +81 -81
- package/src/realtime/events/tournaments.ts +12 -0
- package/src/realtime/index.ts +24 -24
- package/src/realtime/ops/empty.ts +8 -8
- package/src/realtime/ops/hello.ts +40 -40
- package/src/realtime/ops/multiplayer.ts +8 -8
- package/src/realtime/ops/parties.ts +13 -13
- package/src/realtime/ops/races.ts +99 -99
- package/src/realtime/ops/rooms.ts +22 -8
- package/src/realtime/ops/social.ts +58 -58
package/src/api/reports.ts
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reports and suggestions: players flagging problems (multiplayer-impl.md
|
|
3
|
-
* §5.3) and proposing game ideas (§5.5).
|
|
4
|
-
*
|
|
5
|
-
* A report only says *what* is reported; the hub builds the evidence
|
|
6
|
-
* snapshot from its own records. Suggestions are filtered, reviewed by an
|
|
7
|
-
* admin, and only approved ones reach the public board.
|
|
8
|
-
*/
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
import { Millis, Ok } from '../common';
|
|
11
|
-
import { LooseNumber } from '../coerce';
|
|
12
|
-
import { ContentRejected, ErrorBody } from '../errors';
|
|
13
|
-
import { requireActive, requireClaimed, session } from '../middleware';
|
|
14
|
-
import { Presence } from './social/presence';
|
|
15
|
-
|
|
16
|
-
/** What is being reported. */
|
|
17
|
-
export const ReportKind = z.enum(['message', 'user', 'username', 'suggestion', 'room_chat', 'presence'], {
|
|
18
|
-
error: 'kind must be one of message, user, username, suggestion, room_chat, presence.',
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
/** A report kind. */
|
|
22
|
-
export type ReportKind = z.infer<typeof ReportKind>;
|
|
23
|
-
|
|
24
|
-
/** Why, from a fixed list (there is no free text). */
|
|
25
|
-
export const ReportReason = z.enum(['mean', 'bad_words', 'personal', 'inappropriate', 'spam', 'other'], {
|
|
26
|
-
error: 'reason must be one of mean, bad_words, personal, inappropriate, spam, other.',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
/** A report reason. */
|
|
30
|
-
export type ReportReason = z.infer<typeof ReportReason>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* `POST /social/reports`. Loose on purpose: anything else the client sends
|
|
34
|
-
* (an evidence snapshot, quoted text) is ignored, because the hub builds the
|
|
35
|
-
* snapshot from its own records.
|
|
36
|
-
*/
|
|
37
|
-
export const ReportInput = z.looseObject({
|
|
38
|
-
kind: ReportKind,
|
|
39
|
-
/**
|
|
40
|
-
* The reported player. `0` for a suggestion: the hub finds its author from
|
|
41
|
-
* `refId`. Read like `Number(x)`: anything that is not a player's id is a 404.
|
|
42
|
-
*/
|
|
43
|
-
targetUserId: LooseNumber.catch(Number.NaN),
|
|
44
|
-
/**
|
|
45
|
-
* What exactly: the message id (`message`), the suggestion id
|
|
46
|
-
* (`suggestion`) or the room code (`room_chat`). Kept to 64 characters.
|
|
47
|
-
*/
|
|
48
|
-
refId: z.union([z.string(), z.number()]).nullable().optional(),
|
|
49
|
-
reason: ReportReason,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** A report. */
|
|
53
|
-
export type ReportInput = z.infer<typeof ReportInput>;
|
|
54
|
-
|
|
55
|
-
/** A direct message as a report snapshot keeps it. */
|
|
56
|
-
export const ReportedMessage = z.object({
|
|
57
|
-
id: z.number().int().positive(),
|
|
58
|
-
/** The sender's user id. */
|
|
59
|
-
from: z.number().int(),
|
|
60
|
-
/** The recipient's user id. */
|
|
61
|
-
to: z.number().int(),
|
|
62
|
-
/** When it was sent (epoch ms). */
|
|
63
|
-
at: Millis,
|
|
64
|
-
/** The text, or the quick-chat phrase's text (`''` for an unknown phrase). */
|
|
65
|
-
text: z.string(),
|
|
66
|
-
/** The quick-chat phrase id, when it was one. */
|
|
67
|
-
quick: z.string().optional(),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
/** A reported direct message. */
|
|
71
|
-
export type ReportedMessage = z.infer<typeof ReportedMessage>;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The evidence the hub records with a report, from its own records (stored
|
|
75
|
-
* as `snapshot_json`; the admin console shows it). Always the reported
|
|
76
|
-
* player; then per kind: `message` adds the message and up to 5 before it in
|
|
77
|
-
* the conversation (oldest first), `suggestion` its title, body and status,
|
|
78
|
-
* `presence` their status line (`null` when offline), `room_chat` the room
|
|
79
|
-
* code (room chat isn't stored).
|
|
80
|
-
*/
|
|
81
|
-
export const ReportSnapshot = z.object({
|
|
82
|
-
userId: z.number().int(),
|
|
83
|
-
username: z.string(),
|
|
84
|
-
message: ReportedMessage.optional(),
|
|
85
|
-
context: z.array(ReportedMessage).optional(),
|
|
86
|
-
title: z.string().optional(),
|
|
87
|
-
body: z.string().optional(),
|
|
88
|
-
status: z.string().optional(),
|
|
89
|
-
detail: z.string().optional(),
|
|
90
|
-
presence: Presence.nullable().optional(),
|
|
91
|
-
room: z.string().optional(),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/** A report's evidence snapshot. */
|
|
95
|
-
export type ReportSnapshot = z.infer<typeof ReportSnapshot>;
|
|
96
|
-
|
|
97
|
-
/** A suggestion's review status. Only `approved`, `planned` and `done` are on the public board. */
|
|
98
|
-
export const SuggestionStatus = z.enum(['pending', 'approved', 'planned', 'done', 'removed', 'withdrawn']);
|
|
99
|
-
|
|
100
|
-
/** A suggestion status. */
|
|
101
|
-
export type SuggestionStatus = z.infer<typeof SuggestionStatus>;
|
|
102
|
-
|
|
103
|
-
/** A suggestion id. */
|
|
104
|
-
export const SuggestionId = z.number().int().positive();
|
|
105
|
-
|
|
106
|
-
/** A game suggestion. */
|
|
107
|
-
export const Suggestion = z.object({
|
|
108
|
-
id: SuggestionId,
|
|
109
|
-
title: z.string(),
|
|
110
|
-
/** The description. */
|
|
111
|
-
body: z.string(),
|
|
112
|
-
status: SuggestionStatus,
|
|
113
|
-
/** Upvotes. */
|
|
114
|
-
votes: z.number().int().nonnegative(),
|
|
115
|
-
/** You upvoted it. */
|
|
116
|
-
voted: z.boolean(),
|
|
117
|
-
/** You suggested it. */
|
|
118
|
-
mine: z.boolean(),
|
|
119
|
-
/** The reviewer's note to the author, `''` when none. */
|
|
120
|
-
adminNote: z.string(),
|
|
121
|
-
/** When it was suggested (epoch ms). */
|
|
122
|
-
createdAt: Millis,
|
|
123
|
-
/** The author's username: only once it is on the public board (and always in admin views). */
|
|
124
|
-
author: z.string().optional(),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
/** A game suggestion. */
|
|
128
|
-
export type Suggestion = z.infer<typeof Suggestion>;
|
|
129
|
-
|
|
130
|
-
/** A vote's result: the new count and whether you are voting. */
|
|
131
|
-
export const VoteResult = z.object({ votes: z.number().int().nonnegative(), voted: z.boolean() });
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* `:id`, a suggestion: one on the public board to vote, your own pending one
|
|
135
|
-
* to withdraw. Read like `Number(x)`: anything that isn't one is a 404.
|
|
136
|
-
*/
|
|
137
|
-
const SuggestionParams = z.object({ id: LooseNumber.catch(Number.NaN) });
|
|
138
|
-
|
|
139
|
-
/** The old hub's one message for a missing or non-text title or description. */
|
|
140
|
-
const SuggestionText = z.string({ error: 'A suggestion needs a title and a description.' });
|
|
141
|
-
|
|
142
|
-
/** The report and suggestion endpoints. */
|
|
143
|
-
export const reportsEndpoints = {
|
|
144
|
-
...requireActive.group({
|
|
145
|
-
fileReport: {
|
|
146
|
-
method: 'POST', path: '/social/reports', body: ReportInput,
|
|
147
|
-
response: z.object({
|
|
148
|
-
ok: z.literal(true),
|
|
149
|
-
/** The report's id (the open one, when you already reported the same thing). */
|
|
150
|
-
id: z.number().int().positive(),
|
|
151
|
-
}),
|
|
152
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 429: ErrorBody },
|
|
153
|
-
doc: {
|
|
154
|
-
summary: 'Report a player, a message, a suggestion or a status line',
|
|
155
|
-
description: '404 when `refId` is nothing you could have seen from that player.',
|
|
156
|
-
tags: ['reports'],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
160
|
-
...session.group({
|
|
161
|
-
listPublicSuggestions: {
|
|
162
|
-
method: 'GET', path: '/suggestions',
|
|
163
|
-
query: z.object({
|
|
164
|
-
/** `votes` (default) or `new`. */
|
|
165
|
-
sort: z.enum(['votes', 'new']).optional().catch(undefined),
|
|
166
|
-
}),
|
|
167
|
-
response: z.object({
|
|
168
|
-
/** The public board (approved, planned, done), at most 200. Ideas from suspended authors drop off. */
|
|
169
|
-
approved: z.array(Suggestion),
|
|
170
|
-
/** Your own ideas except withdrawn ones, newest first; empty unless signed in to a claimed account. */
|
|
171
|
-
mine: z.array(Suggestion),
|
|
172
|
-
}),
|
|
173
|
-
errors: { 403: ErrorBody },
|
|
174
|
-
doc: { summary: 'The ideas board, plus your own ideas', tags: ['suggestions'] },
|
|
175
|
-
},
|
|
176
|
-
}),
|
|
177
|
-
...requireClaimed.group({
|
|
178
|
-
submitSuggestion: {
|
|
179
|
-
method: 'POST', path: '/suggestions',
|
|
180
|
-
body: z.object({ title: SuggestionText, body: SuggestionText }),
|
|
181
|
-
response: z.object({ suggestion: Suggestion }),
|
|
182
|
-
errors: {
|
|
183
|
-
400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 409: ErrorBody,
|
|
184
|
-
422: ContentRejected, 429: ErrorBody,
|
|
185
|
-
},
|
|
186
|
-
doc: {
|
|
187
|
-
summary: 'Suggest a game idea (filtered; a few a day, a capped number pending)',
|
|
188
|
-
description: '409 `too_many_pending`; 429 `rate_limited` with `retryAfterMs` past the daily limit.',
|
|
189
|
-
tags: ['suggestions'],
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
voteForSuggestion: {
|
|
193
|
-
method: 'POST', path: '/suggestions/:id/vote', params: SuggestionParams, response: VoteResult,
|
|
194
|
-
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
195
|
-
doc: { summary: 'Upvote an idea (idempotent)', tags: ['suggestions'] },
|
|
196
|
-
},
|
|
197
|
-
removeSuggestionVote: {
|
|
198
|
-
method: 'DELETE', path: '/suggestions/:id/vote', params: SuggestionParams, response: VoteResult,
|
|
199
|
-
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
200
|
-
doc: { summary: 'Take an upvote back (idempotent)', tags: ['suggestions'] },
|
|
201
|
-
},
|
|
202
|
-
withdrawSuggestion: {
|
|
203
|
-
method: 'DELETE', path: '/suggestions/:id', params: SuggestionParams, response: Ok,
|
|
204
|
-
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
205
|
-
doc: { summary: 'Withdraw your own idea while it is still pending', tags: ['suggestions'] },
|
|
206
|
-
},
|
|
207
|
-
}),
|
|
208
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Reports and suggestions: players flagging problems (multiplayer-impl.md
|
|
3
|
+
* §5.3) and proposing game ideas (§5.5).
|
|
4
|
+
*
|
|
5
|
+
* A report only says *what* is reported; the hub builds the evidence
|
|
6
|
+
* snapshot from its own records. Suggestions are filtered, reviewed by an
|
|
7
|
+
* admin, and only approved ones reach the public board.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Millis, Ok } from '../common';
|
|
11
|
+
import { LooseNumber } from '../coerce';
|
|
12
|
+
import { ContentRejected, ErrorBody } from '../errors';
|
|
13
|
+
import { requireActive, requireClaimed, session } from '../middleware';
|
|
14
|
+
import { Presence } from './social/presence';
|
|
15
|
+
|
|
16
|
+
/** What is being reported. */
|
|
17
|
+
export const ReportKind = z.enum(['message', 'user', 'username', 'suggestion', 'room_chat', 'presence'], {
|
|
18
|
+
error: 'kind must be one of message, user, username, suggestion, room_chat, presence.',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** A report kind. */
|
|
22
|
+
export type ReportKind = z.infer<typeof ReportKind>;
|
|
23
|
+
|
|
24
|
+
/** Why, from a fixed list (there is no free text). */
|
|
25
|
+
export const ReportReason = z.enum(['mean', 'bad_words', 'personal', 'inappropriate', 'spam', 'other'], {
|
|
26
|
+
error: 'reason must be one of mean, bad_words, personal, inappropriate, spam, other.',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** A report reason. */
|
|
30
|
+
export type ReportReason = z.infer<typeof ReportReason>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `POST /social/reports`. Loose on purpose: anything else the client sends
|
|
34
|
+
* (an evidence snapshot, quoted text) is ignored, because the hub builds the
|
|
35
|
+
* snapshot from its own records.
|
|
36
|
+
*/
|
|
37
|
+
export const ReportInput = z.looseObject({
|
|
38
|
+
kind: ReportKind,
|
|
39
|
+
/**
|
|
40
|
+
* The reported player. `0` for a suggestion: the hub finds its author from
|
|
41
|
+
* `refId`. Read like `Number(x)`: anything that is not a player's id is a 404.
|
|
42
|
+
*/
|
|
43
|
+
targetUserId: LooseNumber.catch(Number.NaN),
|
|
44
|
+
/**
|
|
45
|
+
* What exactly: the message id (`message`), the suggestion id
|
|
46
|
+
* (`suggestion`) or the room code (`room_chat`). Kept to 64 characters.
|
|
47
|
+
*/
|
|
48
|
+
refId: z.union([z.string(), z.number()]).nullable().optional(),
|
|
49
|
+
reason: ReportReason,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** A report. */
|
|
53
|
+
export type ReportInput = z.infer<typeof ReportInput>;
|
|
54
|
+
|
|
55
|
+
/** A direct message as a report snapshot keeps it. */
|
|
56
|
+
export const ReportedMessage = z.object({
|
|
57
|
+
id: z.number().int().positive(),
|
|
58
|
+
/** The sender's user id. */
|
|
59
|
+
from: z.number().int(),
|
|
60
|
+
/** The recipient's user id. */
|
|
61
|
+
to: z.number().int(),
|
|
62
|
+
/** When it was sent (epoch ms). */
|
|
63
|
+
at: Millis,
|
|
64
|
+
/** The text, or the quick-chat phrase's text (`''` for an unknown phrase). */
|
|
65
|
+
text: z.string(),
|
|
66
|
+
/** The quick-chat phrase id, when it was one. */
|
|
67
|
+
quick: z.string().optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** A reported direct message. */
|
|
71
|
+
export type ReportedMessage = z.infer<typeof ReportedMessage>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The evidence the hub records with a report, from its own records (stored
|
|
75
|
+
* as `snapshot_json`; the admin console shows it). Always the reported
|
|
76
|
+
* player; then per kind: `message` adds the message and up to 5 before it in
|
|
77
|
+
* the conversation (oldest first), `suggestion` its title, body and status,
|
|
78
|
+
* `presence` their status line (`null` when offline), `room_chat` the room
|
|
79
|
+
* code (room chat isn't stored).
|
|
80
|
+
*/
|
|
81
|
+
export const ReportSnapshot = z.object({
|
|
82
|
+
userId: z.number().int(),
|
|
83
|
+
username: z.string(),
|
|
84
|
+
message: ReportedMessage.optional(),
|
|
85
|
+
context: z.array(ReportedMessage).optional(),
|
|
86
|
+
title: z.string().optional(),
|
|
87
|
+
body: z.string().optional(),
|
|
88
|
+
status: z.string().optional(),
|
|
89
|
+
detail: z.string().optional(),
|
|
90
|
+
presence: Presence.nullable().optional(),
|
|
91
|
+
room: z.string().optional(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** A report's evidence snapshot. */
|
|
95
|
+
export type ReportSnapshot = z.infer<typeof ReportSnapshot>;
|
|
96
|
+
|
|
97
|
+
/** A suggestion's review status. Only `approved`, `planned` and `done` are on the public board. */
|
|
98
|
+
export const SuggestionStatus = z.enum(['pending', 'approved', 'planned', 'done', 'removed', 'withdrawn']);
|
|
99
|
+
|
|
100
|
+
/** A suggestion status. */
|
|
101
|
+
export type SuggestionStatus = z.infer<typeof SuggestionStatus>;
|
|
102
|
+
|
|
103
|
+
/** A suggestion id. */
|
|
104
|
+
export const SuggestionId = z.number().int().positive();
|
|
105
|
+
|
|
106
|
+
/** A game suggestion. */
|
|
107
|
+
export const Suggestion = z.object({
|
|
108
|
+
id: SuggestionId,
|
|
109
|
+
title: z.string(),
|
|
110
|
+
/** The description. */
|
|
111
|
+
body: z.string(),
|
|
112
|
+
status: SuggestionStatus,
|
|
113
|
+
/** Upvotes. */
|
|
114
|
+
votes: z.number().int().nonnegative(),
|
|
115
|
+
/** You upvoted it. */
|
|
116
|
+
voted: z.boolean(),
|
|
117
|
+
/** You suggested it. */
|
|
118
|
+
mine: z.boolean(),
|
|
119
|
+
/** The reviewer's note to the author, `''` when none. */
|
|
120
|
+
adminNote: z.string(),
|
|
121
|
+
/** When it was suggested (epoch ms). */
|
|
122
|
+
createdAt: Millis,
|
|
123
|
+
/** The author's username: only once it is on the public board (and always in admin views). */
|
|
124
|
+
author: z.string().optional(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** A game suggestion. */
|
|
128
|
+
export type Suggestion = z.infer<typeof Suggestion>;
|
|
129
|
+
|
|
130
|
+
/** A vote's result: the new count and whether you are voting. */
|
|
131
|
+
export const VoteResult = z.object({ votes: z.number().int().nonnegative(), voted: z.boolean() });
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `:id`, a suggestion: one on the public board to vote, your own pending one
|
|
135
|
+
* to withdraw. Read like `Number(x)`: anything that isn't one is a 404.
|
|
136
|
+
*/
|
|
137
|
+
const SuggestionParams = z.object({ id: LooseNumber.catch(Number.NaN) });
|
|
138
|
+
|
|
139
|
+
/** The old hub's one message for a missing or non-text title or description. */
|
|
140
|
+
const SuggestionText = z.string({ error: 'A suggestion needs a title and a description.' });
|
|
141
|
+
|
|
142
|
+
/** The report and suggestion endpoints. */
|
|
143
|
+
export const reportsEndpoints = {
|
|
144
|
+
...requireActive.group({
|
|
145
|
+
fileReport: {
|
|
146
|
+
method: 'POST', path: '/social/reports', body: ReportInput,
|
|
147
|
+
response: z.object({
|
|
148
|
+
ok: z.literal(true),
|
|
149
|
+
/** The report's id (the open one, when you already reported the same thing). */
|
|
150
|
+
id: z.number().int().positive(),
|
|
151
|
+
}),
|
|
152
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 429: ErrorBody },
|
|
153
|
+
doc: {
|
|
154
|
+
summary: 'Report a player, a message, a suggestion or a status line',
|
|
155
|
+
description: '404 when `refId` is nothing you could have seen from that player.',
|
|
156
|
+
tags: ['reports'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
...session.group({
|
|
161
|
+
listPublicSuggestions: {
|
|
162
|
+
method: 'GET', path: '/suggestions',
|
|
163
|
+
query: z.object({
|
|
164
|
+
/** `votes` (default) or `new`. */
|
|
165
|
+
sort: z.enum(['votes', 'new']).optional().catch(undefined),
|
|
166
|
+
}),
|
|
167
|
+
response: z.object({
|
|
168
|
+
/** The public board (approved, planned, done), at most 200. Ideas from suspended authors drop off. */
|
|
169
|
+
approved: z.array(Suggestion),
|
|
170
|
+
/** Your own ideas except withdrawn ones, newest first; empty unless signed in to a claimed account. */
|
|
171
|
+
mine: z.array(Suggestion),
|
|
172
|
+
}),
|
|
173
|
+
errors: { 403: ErrorBody },
|
|
174
|
+
doc: { summary: 'The ideas board, plus your own ideas', tags: ['suggestions'] },
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
...requireClaimed.group({
|
|
178
|
+
submitSuggestion: {
|
|
179
|
+
method: 'POST', path: '/suggestions',
|
|
180
|
+
body: z.object({ title: SuggestionText, body: SuggestionText }),
|
|
181
|
+
response: z.object({ suggestion: Suggestion }),
|
|
182
|
+
errors: {
|
|
183
|
+
400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 409: ErrorBody,
|
|
184
|
+
422: ContentRejected, 429: ErrorBody,
|
|
185
|
+
},
|
|
186
|
+
doc: {
|
|
187
|
+
summary: 'Suggest a game idea (filtered; a few a day, a capped number pending)',
|
|
188
|
+
description: '409 `too_many_pending`; 429 `rate_limited` with `retryAfterMs` past the daily limit.',
|
|
189
|
+
tags: ['suggestions'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
voteForSuggestion: {
|
|
193
|
+
method: 'POST', path: '/suggestions/:id/vote', params: SuggestionParams, response: VoteResult,
|
|
194
|
+
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
195
|
+
doc: { summary: 'Upvote an idea (idempotent)', tags: ['suggestions'] },
|
|
196
|
+
},
|
|
197
|
+
removeSuggestionVote: {
|
|
198
|
+
method: 'DELETE', path: '/suggestions/:id/vote', params: SuggestionParams, response: VoteResult,
|
|
199
|
+
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
200
|
+
doc: { summary: 'Take an upvote back (idempotent)', tags: ['suggestions'] },
|
|
201
|
+
},
|
|
202
|
+
withdrawSuggestion: {
|
|
203
|
+
method: 'DELETE', path: '/suggestions/:id', params: SuggestionParams, response: Ok,
|
|
204
|
+
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
205
|
+
doc: { summary: 'Withdraw your own idea while it is still pending', tags: ['suggestions'] },
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
};
|
package/src/api/social/config.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The site-wide social configuration every social UI renders from
|
|
3
|
-
* (`GET /social/config`, `ev:config`, the `hello` snapshot).
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { ChatMode } from '../../entities';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** A quick-chat phrase id: a hub phrase (`gg`), an emote (`e:<emoji>`) or a game phrase (`g:<slug>:<n>`). */
|
|
10
|
-
export const QuickId = z.string().min(1);
|
|
11
|
-
|
|
12
|
-
/** A non-negative character limit. */
|
|
13
|
-
const CharLimit = z.number().int().nonnegative();
|
|
14
|
-
|
|
15
|
-
/** Text length limits, in characters. */
|
|
16
|
-
export const SocialLimits = z.object({
|
|
17
|
-
dmMaxChars: CharLimit,
|
|
18
|
-
roomChatMaxChars: CharLimit,
|
|
19
|
-
suggestionTitleMax: CharLimit,
|
|
20
|
-
suggestionBodyMax: CharLimit,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/** The site-wide social configuration. */
|
|
24
|
-
export const SocialConfig = z.object({
|
|
25
|
-
/** The site-wide chat mode (operator setting tightened by the admin switch). */
|
|
26
|
-
chatMode: ChatMode,
|
|
27
|
-
/** The AI safety reviewer is configured. */
|
|
28
|
-
ai: z.boolean(),
|
|
29
|
-
/** Where parents can reach the operator, `''` when unset. */
|
|
30
|
-
contactUrl: z.string(),
|
|
31
|
-
/** The public "how chat is kept safe" page. */
|
|
32
|
-
parentsUrl: z.string(),
|
|
33
|
-
/** The Web Push VAPID public key (base64url), `''` when push isn't configured. */
|
|
34
|
-
vapidPublicKey: z.string(),
|
|
35
|
-
/** Every quick-chat phrase, id to text: hub phrases, emotes and every game's phrases. */
|
|
36
|
-
quickChat: z.record(QuickId, z.string()),
|
|
37
|
-
/** The emote emoji (also in `quickChat` as `e:<emoji>` ids). */
|
|
38
|
-
emotes: z.array(z.string()),
|
|
39
|
-
limits: SocialLimits,
|
|
40
|
-
/** The preset avatar ids (`a0` to `a23`). */
|
|
41
|
-
avatars: z.array(z.string()),
|
|
42
|
-
/** Present (`true`) only when the host runs in test mode (`HUB_TEST=1`). */
|
|
43
|
-
test: z.literal(true).optional(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
/** The social configuration. */
|
|
47
|
-
export type SocialConfig = z.infer<typeof SocialConfig>;
|
|
1
|
+
/**
|
|
2
|
+
* The site-wide social configuration every social UI renders from
|
|
3
|
+
* (`GET /social/config`, `ev:config`, the `hello` snapshot).
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ChatMode } from '../../entities';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/** A quick-chat phrase id: a hub phrase (`gg`), an emote (`e:<emoji>`) or a game phrase (`g:<slug>:<n>`). */
|
|
10
|
+
export const QuickId = z.string().min(1);
|
|
11
|
+
|
|
12
|
+
/** A non-negative character limit. */
|
|
13
|
+
const CharLimit = z.number().int().nonnegative();
|
|
14
|
+
|
|
15
|
+
/** Text length limits, in characters. */
|
|
16
|
+
export const SocialLimits = z.object({
|
|
17
|
+
dmMaxChars: CharLimit,
|
|
18
|
+
roomChatMaxChars: CharLimit,
|
|
19
|
+
suggestionTitleMax: CharLimit,
|
|
20
|
+
suggestionBodyMax: CharLimit,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** The site-wide social configuration. */
|
|
24
|
+
export const SocialConfig = z.object({
|
|
25
|
+
/** The site-wide chat mode (operator setting tightened by the admin switch). */
|
|
26
|
+
chatMode: ChatMode,
|
|
27
|
+
/** The AI safety reviewer is configured. */
|
|
28
|
+
ai: z.boolean(),
|
|
29
|
+
/** Where parents can reach the operator, `''` when unset. */
|
|
30
|
+
contactUrl: z.string(),
|
|
31
|
+
/** The public "how chat is kept safe" page. */
|
|
32
|
+
parentsUrl: z.string(),
|
|
33
|
+
/** The Web Push VAPID public key (base64url), `''` when push isn't configured. */
|
|
34
|
+
vapidPublicKey: z.string(),
|
|
35
|
+
/** Every quick-chat phrase, id to text: hub phrases, emotes and every game's phrases. */
|
|
36
|
+
quickChat: z.record(QuickId, z.string()),
|
|
37
|
+
/** The emote emoji (also in `quickChat` as `e:<emoji>` ids). */
|
|
38
|
+
emotes: z.array(z.string()),
|
|
39
|
+
limits: SocialLimits,
|
|
40
|
+
/** The preset avatar ids (`a0` to `a23`). */
|
|
41
|
+
avatars: z.array(z.string()),
|
|
42
|
+
/** Present (`true`) only when the host runs in test mode (`HUB_TEST=1`). */
|
|
43
|
+
test: z.literal(true).optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The social configuration. */
|
|
47
|
+
export type SocialConfig = z.infer<typeof SocialConfig>;
|