@diffenderfer-games/hub-contract 2.0.0-next.0 → 2.0.0-next.1
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 +7 -0
- package/LICENSE +21 -21
- package/README.md +26 -26
- package/dist/index.d.ts +95 -95
- package/package.json +53 -53
- package/src/api/analytics.ts +81 -81
- package/src/api/auth.ts +77 -77
- package/src/api/catalog.ts +171 -171
- package/src/api/daily.ts +122 -122
- package/src/api/economy/bundle.ts +69 -69
- package/src/api/index.ts +85 -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/races.ts +211 -211
- package/src/api/mp/rooms.ts +90 -90
- package/src/api/mp/snapshot.ts +17 -17
- package/src/api/mp.ts +110 -110
- package/src/api/notify.ts +111 -111
- 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/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 +43 -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 +100 -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 +132 -132
- package/src/manifest/index.ts +12 -12
- package/src/manifest/inject.ts +31 -31
- package/src/manifest/leaderboards.ts +29 -29
- package/src/manifest/multiplayer.ts +71 -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 +8 -8
- package/src/realtime/events/parties.ts +31 -31
- package/src/realtime/events/races.ts +27 -27
- package/src/realtime/events/rooms.ts +48 -48
- package/src/realtime/events/social.ts +81 -81
- 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 +1 -1
- package/src/realtime/ops/social.ts +58 -58
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Social core REST (multiplayer-impl.md §5.1): config, your own social
|
|
3
|
-
* settings, friends and requests, blocks, player cards, recent players and
|
|
4
|
-
* online counts. Every route here is closed to suspended accounts (403
|
|
5
|
-
* `suspended`) except `GET /social/config` and `GET /social/me`, which a
|
|
6
|
-
* suspended player's UI needs to explain what happened.
|
|
7
|
-
*/
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
import { Ok } from '../../common';
|
|
10
|
-
import { requiredId } from '../../coerce';
|
|
11
|
-
import { ContentRejected, ErrorBody } from '../../errors';
|
|
12
|
-
import { requireActive, requireClaimed, requireUser, session } from '../../middleware';
|
|
13
|
-
import { NotifyPrefs } from '../notify';
|
|
14
|
-
import { SocialConfig } from './config';
|
|
15
|
-
import { Audience, FriendsList, PlayerCard, RecentPlayer, SocialMe } from './people';
|
|
16
|
-
import { Counts } from './presence';
|
|
17
|
-
|
|
18
|
-
/** `{ me }`: your social settings after a change. */
|
|
19
|
-
export const SocialMeResult = z.object({ me: SocialMe });
|
|
20
|
-
|
|
21
|
-
/** `:id`, another player's user id. A malformed id is a 400. */
|
|
22
|
-
const OtherUser = z.object({ id: requiredId('id must be a user id.') });
|
|
23
|
-
|
|
24
|
-
/** An {@link Audience} field, with the old hub's message for anything else. */
|
|
25
|
-
const audienceField = (field: string) => z.enum(Audience.options, { error: `${field} must be friends or nobody.` });
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* `POST /social/friends`: who to befriend, by id or by name, and an optional
|
|
29
|
-
* first message. `userId` wins when both are given; with neither, the player
|
|
30
|
-
* is not found (404 `no_player`).
|
|
31
|
-
*/
|
|
32
|
-
export const FriendRequestInput = z.object({
|
|
33
|
-
userId: requiredId('userId must be a user id.').nullish(),
|
|
34
|
-
/** A username, case-insensitive. */
|
|
35
|
-
username: z.string().optional(),
|
|
36
|
-
message: z.string().optional(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/** A friend request. */
|
|
40
|
-
export type FriendRequestInput = z.infer<typeof FriendRequestInput>;
|
|
41
|
-
|
|
42
|
-
/** The result of `POST /social/friends`. */
|
|
43
|
-
export const FriendRequestStatus = z.enum(['requested', 'friends']);
|
|
44
|
-
|
|
45
|
-
/** The social core endpoints. */
|
|
46
|
-
export const socialEndpoints = {
|
|
47
|
-
...session.group({
|
|
48
|
-
getSocialConfig: {
|
|
49
|
-
method: 'GET', path: '/social/config', response: SocialConfig,
|
|
50
|
-
doc: { summary: 'The site-wide social configuration', tags: ['social'] },
|
|
51
|
-
},
|
|
52
|
-
getOnlineCounts: {
|
|
53
|
-
method: 'GET', path: '/social/counts', response: Counts,
|
|
54
|
-
errors: { 403: ErrorBody },
|
|
55
|
-
doc: { summary: 'Anonymous online counts, overall and per game', tags: ['social'] },
|
|
56
|
-
},
|
|
57
|
-
}),
|
|
58
|
-
...requireUser.group({
|
|
59
|
-
getMySocialProfile: {
|
|
60
|
-
method: 'GET', path: '/social/me',
|
|
61
|
-
response: z.object({
|
|
62
|
-
me: SocialMe,
|
|
63
|
-
notify: NotifyPrefs,
|
|
64
|
-
/** How many browsers have a push subscription for this account. */
|
|
65
|
-
pushSubs: z.number().int().nonnegative(),
|
|
66
|
-
}),
|
|
67
|
-
errors: { 401: ErrorBody },
|
|
68
|
-
doc: { summary: 'Your social settings, notification preferences and push subscription count', tags: ['social'] },
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
...requireActive.group({
|
|
72
|
-
listFriendsAndRequests: {
|
|
73
|
-
method: 'GET', path: '/social/friends', response: FriendsList,
|
|
74
|
-
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
75
|
-
doc: { summary: 'Friends (online first), requests both ways and blocks', tags: ['social'] },
|
|
76
|
-
},
|
|
77
|
-
findPlayerByName: {
|
|
78
|
-
method: 'GET', path: '/social/players/by-name/:name',
|
|
79
|
-
params: z.object({ name: z.string() }), response: PlayerCard,
|
|
80
|
-
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
81
|
-
doc: { summary: 'A player card, by username', tags: ['social'] },
|
|
82
|
-
},
|
|
83
|
-
getPlayerCard: {
|
|
84
|
-
method: 'GET', path: '/social/players/:id', params: OtherUser, response: PlayerCard,
|
|
85
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
86
|
-
doc: { summary: 'A player card, by id (404 `no_player` when blocked by them or suspended)', tags: ['social'] },
|
|
87
|
-
},
|
|
88
|
-
}),
|
|
89
|
-
...requireClaimed.group({
|
|
90
|
-
updateMySocialProfile: {
|
|
91
|
-
method: 'PUT', path: '/social/me',
|
|
92
|
-
body: z.object({
|
|
93
|
-
/** One of the config's preset avatar ids (else 400 `bad_avatar`). */
|
|
94
|
-
avatar: z.string().optional(),
|
|
95
|
-
privacy: audienceField('privacy').optional(),
|
|
96
|
-
dmPolicy: audienceField('dmPolicy').optional(),
|
|
97
|
-
}),
|
|
98
|
-
response: SocialMeResult,
|
|
99
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
100
|
-
doc: { summary: 'Change your avatar, presence privacy or who may message you', tags: ['social'] },
|
|
101
|
-
},
|
|
102
|
-
setParentChatLock: {
|
|
103
|
-
method: 'POST', path: '/social/chat-lock',
|
|
104
|
-
body: z.union([
|
|
105
|
-
z.object({ on: z.literal(true) }),
|
|
106
|
-
z.object({
|
|
107
|
-
on: z.literal(false),
|
|
108
|
-
/** The account password; required to turn the lock off. */
|
|
109
|
-
password: z.string().optional(),
|
|
110
|
-
}),
|
|
111
|
-
], { error: 'on must be true or false.' }),
|
|
112
|
-
response: SocialMeResult,
|
|
113
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 429: ErrorBody },
|
|
114
|
-
doc: {
|
|
115
|
-
summary: 'Parent chat lock: on with one tap, off only with the account password',
|
|
116
|
-
description: 'A wrong password is 403 `bad_password` (logged and rate-limited).',
|
|
117
|
-
tags: ['social'],
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
sendFriendRequest: {
|
|
121
|
-
method: 'POST', path: '/social/friends', body: FriendRequestInput,
|
|
122
|
-
response: z.object({ status: FriendRequestStatus }),
|
|
123
|
-
errors: {
|
|
124
|
-
400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody,
|
|
125
|
-
422: ContentRejected, 429: ErrorBody,
|
|
126
|
-
},
|
|
127
|
-
doc: {
|
|
128
|
-
summary: 'Send a friend request (a reverse request accepts at once)',
|
|
129
|
-
description: 'Answers `requested` without doing anything when the target blocked you or is suspended. '
|
|
130
|
-
+ '`message` is moderated now and delivered as the first DM once you are friends.',
|
|
131
|
-
tags: ['social'],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
acceptFriendRequest: {
|
|
135
|
-
method: 'POST', path: '/social/friends/:id/accept', params: OtherUser, response: Ok,
|
|
136
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
137
|
-
doc: { summary: 'Accept the request from `:id`', tags: ['social'] },
|
|
138
|
-
},
|
|
139
|
-
declineFriendRequest: {
|
|
140
|
-
method: 'POST', path: '/social/friends/:id/decline', params: OtherUser, response: Ok,
|
|
141
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
142
|
-
doc: { summary: 'Quietly drop the request from `:id`', tags: ['social'] },
|
|
143
|
-
},
|
|
144
|
-
removeFriend: {
|
|
145
|
-
method: 'DELETE', path: '/social/friends/:id', params: OtherUser, response: Ok,
|
|
146
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
147
|
-
doc: { summary: 'Unfriend `:id`, or cancel your request to them', tags: ['social'] },
|
|
148
|
-
},
|
|
149
|
-
setFriendNotifications: {
|
|
150
|
-
method: 'PUT', path: '/social/friends/:id/notify', params: OtherUser,
|
|
151
|
-
body: z.object({ online: z.boolean({ error: 'online must be true or false.' }) }), response: Ok,
|
|
152
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
153
|
-
doc: { summary: "Turn a friend's \"tell me when they're online\" bell on or off", tags: ['social'] },
|
|
154
|
-
},
|
|
155
|
-
blockPlayer: {
|
|
156
|
-
method: 'POST', path: '/social/blocks', body: z.object({ userId: requiredId('userId must be a user id.') }), response: Ok,
|
|
157
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
158
|
-
doc: {
|
|
159
|
-
summary: 'Block a player',
|
|
160
|
-
description: 'Also unfriends, drops requests both ways and splits a shared party.',
|
|
161
|
-
tags: ['social'],
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
unblockPlayer: {
|
|
165
|
-
method: 'DELETE', path: '/social/blocks/:id', params: OtherUser, response: Ok,
|
|
166
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
167
|
-
doc: { summary: 'Unblock a player', tags: ['social'] },
|
|
168
|
-
},
|
|
169
|
-
listRecentPlayers: {
|
|
170
|
-
method: 'GET', path: '/social/recent',
|
|
171
|
-
response: z.object({
|
|
172
|
-
/** Newest first, at most 50. */
|
|
173
|
-
players: z.array(RecentPlayer),
|
|
174
|
-
}),
|
|
175
|
-
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
176
|
-
doc: { summary: 'People you recently played with', tags: ['social'] },
|
|
177
|
-
},
|
|
178
|
-
}),
|
|
179
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Social core REST (multiplayer-impl.md §5.1): config, your own social
|
|
3
|
+
* settings, friends and requests, blocks, player cards, recent players and
|
|
4
|
+
* online counts. Every route here is closed to suspended accounts (403
|
|
5
|
+
* `suspended`) except `GET /social/config` and `GET /social/me`, which a
|
|
6
|
+
* suspended player's UI needs to explain what happened.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { Ok } from '../../common';
|
|
10
|
+
import { requiredId } from '../../coerce';
|
|
11
|
+
import { ContentRejected, ErrorBody } from '../../errors';
|
|
12
|
+
import { requireActive, requireClaimed, requireUser, session } from '../../middleware';
|
|
13
|
+
import { NotifyPrefs } from '../notify';
|
|
14
|
+
import { SocialConfig } from './config';
|
|
15
|
+
import { Audience, FriendsList, PlayerCard, RecentPlayer, SocialMe } from './people';
|
|
16
|
+
import { Counts } from './presence';
|
|
17
|
+
|
|
18
|
+
/** `{ me }`: your social settings after a change. */
|
|
19
|
+
export const SocialMeResult = z.object({ me: SocialMe });
|
|
20
|
+
|
|
21
|
+
/** `:id`, another player's user id. A malformed id is a 400. */
|
|
22
|
+
const OtherUser = z.object({ id: requiredId('id must be a user id.') });
|
|
23
|
+
|
|
24
|
+
/** An {@link Audience} field, with the old hub's message for anything else. */
|
|
25
|
+
const audienceField = (field: string) => z.enum(Audience.options, { error: `${field} must be friends or nobody.` });
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `POST /social/friends`: who to befriend, by id or by name, and an optional
|
|
29
|
+
* first message. `userId` wins when both are given; with neither, the player
|
|
30
|
+
* is not found (404 `no_player`).
|
|
31
|
+
*/
|
|
32
|
+
export const FriendRequestInput = z.object({
|
|
33
|
+
userId: requiredId('userId must be a user id.').nullish(),
|
|
34
|
+
/** A username, case-insensitive. */
|
|
35
|
+
username: z.string().optional(),
|
|
36
|
+
message: z.string().optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A friend request. */
|
|
40
|
+
export type FriendRequestInput = z.infer<typeof FriendRequestInput>;
|
|
41
|
+
|
|
42
|
+
/** The result of `POST /social/friends`. */
|
|
43
|
+
export const FriendRequestStatus = z.enum(['requested', 'friends']);
|
|
44
|
+
|
|
45
|
+
/** The social core endpoints. */
|
|
46
|
+
export const socialEndpoints = {
|
|
47
|
+
...session.group({
|
|
48
|
+
getSocialConfig: {
|
|
49
|
+
method: 'GET', path: '/social/config', response: SocialConfig,
|
|
50
|
+
doc: { summary: 'The site-wide social configuration', tags: ['social'] },
|
|
51
|
+
},
|
|
52
|
+
getOnlineCounts: {
|
|
53
|
+
method: 'GET', path: '/social/counts', response: Counts,
|
|
54
|
+
errors: { 403: ErrorBody },
|
|
55
|
+
doc: { summary: 'Anonymous online counts, overall and per game', tags: ['social'] },
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
...requireUser.group({
|
|
59
|
+
getMySocialProfile: {
|
|
60
|
+
method: 'GET', path: '/social/me',
|
|
61
|
+
response: z.object({
|
|
62
|
+
me: SocialMe,
|
|
63
|
+
notify: NotifyPrefs,
|
|
64
|
+
/** How many browsers have a push subscription for this account. */
|
|
65
|
+
pushSubs: z.number().int().nonnegative(),
|
|
66
|
+
}),
|
|
67
|
+
errors: { 401: ErrorBody },
|
|
68
|
+
doc: { summary: 'Your social settings, notification preferences and push subscription count', tags: ['social'] },
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
...requireActive.group({
|
|
72
|
+
listFriendsAndRequests: {
|
|
73
|
+
method: 'GET', path: '/social/friends', response: FriendsList,
|
|
74
|
+
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
75
|
+
doc: { summary: 'Friends (online first), requests both ways and blocks', tags: ['social'] },
|
|
76
|
+
},
|
|
77
|
+
findPlayerByName: {
|
|
78
|
+
method: 'GET', path: '/social/players/by-name/:name',
|
|
79
|
+
params: z.object({ name: z.string() }), response: PlayerCard,
|
|
80
|
+
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
81
|
+
doc: { summary: 'A player card, by username', tags: ['social'] },
|
|
82
|
+
},
|
|
83
|
+
getPlayerCard: {
|
|
84
|
+
method: 'GET', path: '/social/players/:id', params: OtherUser, response: PlayerCard,
|
|
85
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
86
|
+
doc: { summary: 'A player card, by id (404 `no_player` when blocked by them or suspended)', tags: ['social'] },
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
...requireClaimed.group({
|
|
90
|
+
updateMySocialProfile: {
|
|
91
|
+
method: 'PUT', path: '/social/me',
|
|
92
|
+
body: z.object({
|
|
93
|
+
/** One of the config's preset avatar ids (else 400 `bad_avatar`). */
|
|
94
|
+
avatar: z.string().optional(),
|
|
95
|
+
privacy: audienceField('privacy').optional(),
|
|
96
|
+
dmPolicy: audienceField('dmPolicy').optional(),
|
|
97
|
+
}),
|
|
98
|
+
response: SocialMeResult,
|
|
99
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
100
|
+
doc: { summary: 'Change your avatar, presence privacy or who may message you', tags: ['social'] },
|
|
101
|
+
},
|
|
102
|
+
setParentChatLock: {
|
|
103
|
+
method: 'POST', path: '/social/chat-lock',
|
|
104
|
+
body: z.union([
|
|
105
|
+
z.object({ on: z.literal(true) }),
|
|
106
|
+
z.object({
|
|
107
|
+
on: z.literal(false),
|
|
108
|
+
/** The account password; required to turn the lock off. */
|
|
109
|
+
password: z.string().optional(),
|
|
110
|
+
}),
|
|
111
|
+
], { error: 'on must be true or false.' }),
|
|
112
|
+
response: SocialMeResult,
|
|
113
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 429: ErrorBody },
|
|
114
|
+
doc: {
|
|
115
|
+
summary: 'Parent chat lock: on with one tap, off only with the account password',
|
|
116
|
+
description: 'A wrong password is 403 `bad_password` (logged and rate-limited).',
|
|
117
|
+
tags: ['social'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
sendFriendRequest: {
|
|
121
|
+
method: 'POST', path: '/social/friends', body: FriendRequestInput,
|
|
122
|
+
response: z.object({ status: FriendRequestStatus }),
|
|
123
|
+
errors: {
|
|
124
|
+
400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody,
|
|
125
|
+
422: ContentRejected, 429: ErrorBody,
|
|
126
|
+
},
|
|
127
|
+
doc: {
|
|
128
|
+
summary: 'Send a friend request (a reverse request accepts at once)',
|
|
129
|
+
description: 'Answers `requested` without doing anything when the target blocked you or is suspended. '
|
|
130
|
+
+ '`message` is moderated now and delivered as the first DM once you are friends.',
|
|
131
|
+
tags: ['social'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
acceptFriendRequest: {
|
|
135
|
+
method: 'POST', path: '/social/friends/:id/accept', params: OtherUser, response: Ok,
|
|
136
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
137
|
+
doc: { summary: 'Accept the request from `:id`', tags: ['social'] },
|
|
138
|
+
},
|
|
139
|
+
declineFriendRequest: {
|
|
140
|
+
method: 'POST', path: '/social/friends/:id/decline', params: OtherUser, response: Ok,
|
|
141
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
142
|
+
doc: { summary: 'Quietly drop the request from `:id`', tags: ['social'] },
|
|
143
|
+
},
|
|
144
|
+
removeFriend: {
|
|
145
|
+
method: 'DELETE', path: '/social/friends/:id', params: OtherUser, response: Ok,
|
|
146
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
147
|
+
doc: { summary: 'Unfriend `:id`, or cancel your request to them', tags: ['social'] },
|
|
148
|
+
},
|
|
149
|
+
setFriendNotifications: {
|
|
150
|
+
method: 'PUT', path: '/social/friends/:id/notify', params: OtherUser,
|
|
151
|
+
body: z.object({ online: z.boolean({ error: 'online must be true or false.' }) }), response: Ok,
|
|
152
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
153
|
+
doc: { summary: "Turn a friend's \"tell me when they're online\" bell on or off", tags: ['social'] },
|
|
154
|
+
},
|
|
155
|
+
blockPlayer: {
|
|
156
|
+
method: 'POST', path: '/social/blocks', body: z.object({ userId: requiredId('userId must be a user id.') }), response: Ok,
|
|
157
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
|
|
158
|
+
doc: {
|
|
159
|
+
summary: 'Block a player',
|
|
160
|
+
description: 'Also unfriends, drops requests both ways and splits a shared party.',
|
|
161
|
+
tags: ['social'],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
unblockPlayer: {
|
|
165
|
+
method: 'DELETE', path: '/social/blocks/:id', params: OtherUser, response: Ok,
|
|
166
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
|
|
167
|
+
doc: { summary: 'Unblock a player', tags: ['social'] },
|
|
168
|
+
},
|
|
169
|
+
listRecentPlayers: {
|
|
170
|
+
method: 'GET', path: '/social/recent',
|
|
171
|
+
response: z.object({
|
|
172
|
+
/** Newest first, at most 50. */
|
|
173
|
+
players: z.array(RecentPlayer),
|
|
174
|
+
}),
|
|
175
|
+
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
176
|
+
doc: { summary: 'People you recently played with', tags: ['social'] },
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
};
|
package/src/api/social/people.ts
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* People shapes: your own social settings, friends, requests, player cards
|
|
3
|
-
* and recent players. Several are also part of the socket `hello` snapshot.
|
|
4
|
-
*
|
|
5
|
-
* Privacy rules behind them: a block in either direction looks to the
|
|
6
|
-
* blocked side like an ordinary unfriend (or a missing player), suspended
|
|
7
|
-
* players are invisible, and guests have no friends.
|
|
8
|
-
*/
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
import { Millis, Slug } from '../../common';
|
|
11
|
-
import { PlayerLite, SessionUser } from '../../entities';
|
|
12
|
-
import { Presence } from './presence';
|
|
13
|
-
|
|
14
|
-
/** Who may see my presence, or message me: friends, or nobody. */
|
|
15
|
-
export const Audience = z.enum(['friends', 'nobody']);
|
|
16
|
-
|
|
17
|
-
/** An audience setting. */
|
|
18
|
-
export type Audience = z.infer<typeof Audience>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Your own account's social fields (`GET`/`PUT /social/me`, the `hello`
|
|
22
|
-
* snapshot's `me`). Never shown to other players.
|
|
23
|
-
*/
|
|
24
|
-
export const SocialMe = SessionUser.extend({
|
|
25
|
-
/** Avatar token; never empty (a default is derived from the id). */
|
|
26
|
-
avatar: PlayerLite.shape.avatar,
|
|
27
|
-
/** Who sees my presence and last-seen time. */
|
|
28
|
-
privacy: Audience,
|
|
29
|
-
/** Who may message me (and invite me). */
|
|
30
|
-
dmPolicy: Audience,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
/** Your own social settings. */
|
|
34
|
-
export type SocialMe = z.infer<typeof SocialMe>;
|
|
35
|
-
|
|
36
|
-
/** A friend, with what you may see of their presence. Online friends sort first. */
|
|
37
|
-
export const FriendEntry = PlayerLite.extend({
|
|
38
|
-
/** `null` when offline or hiding their presence. */
|
|
39
|
-
presence: Presence.nullable(),
|
|
40
|
-
/** "Tell me when they come online" is on for this friend. */
|
|
41
|
-
notifyOnline: z.boolean(),
|
|
42
|
-
/** When you became friends (epoch ms). */
|
|
43
|
-
since: Millis,
|
|
44
|
-
/** When they were last online (epoch ms); `0` when unknown or they hide their presence. */
|
|
45
|
-
lastSeenAt: Millis,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** A friend. */
|
|
49
|
-
export type FriendEntry = z.infer<typeof FriendEntry>;
|
|
50
|
-
|
|
51
|
-
/** Pending friend requests both ways, newest first (blocked and suspended players left out). */
|
|
52
|
-
export const FriendRequests = z.object({
|
|
53
|
-
/** Requests waiting for you. */
|
|
54
|
-
incoming: z.array(PlayerLite),
|
|
55
|
-
/** Requests you sent. */
|
|
56
|
-
outgoing: z.array(PlayerLite),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/** Pending friend requests. */
|
|
60
|
-
export type FriendRequests = z.infer<typeof FriendRequests>;
|
|
61
|
-
|
|
62
|
-
/** `GET /social/friends`: friends, requests and the players you blocked. Guests get empty lists. */
|
|
63
|
-
export const FriendsList = FriendRequests.extend({
|
|
64
|
-
friends: z.array(FriendEntry),
|
|
65
|
-
/** Players you blocked, newest first. */
|
|
66
|
-
blocked: z.array(PlayerLite),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/** Friends, requests and blocks. */
|
|
70
|
-
export type FriendsList = z.infer<typeof FriendsList>;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* What changed in `ev:friends` (the client refetches `GET /social/friends`).
|
|
74
|
-
* `restored`: a friend's suspension ended. A player you blocked only ever
|
|
75
|
-
* sees `removed`.
|
|
76
|
-
*/
|
|
77
|
-
export const FriendChange = z.enum(['request', 'accepted', 'removed', 'declined', 'cancelled', 'blocked', 'restored']);
|
|
78
|
-
|
|
79
|
-
/** A friends-list change. */
|
|
80
|
-
export type FriendChange = z.infer<typeof FriendChange>;
|
|
81
|
-
|
|
82
|
-
/** My relation to another player, as their card shows it. */
|
|
83
|
-
export const Relation = z.enum(['self', 'friend', 'incoming', 'outgoing', 'blocked', 'none']);
|
|
84
|
-
|
|
85
|
-
/** A relation. */
|
|
86
|
-
export type Relation = z.infer<typeof Relation>;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* A player card (`GET /social/players/:id`). Someone who blocked you, or who
|
|
90
|
-
* is suspended, answers 404 exactly like a player who doesn't exist.
|
|
91
|
-
*/
|
|
92
|
-
export const PlayerCard = PlayerLite.extend({
|
|
93
|
-
relation: Relation,
|
|
94
|
-
/** Friends only; `null` otherwise, or when offline or hidden. */
|
|
95
|
-
presence: Presence.nullable(),
|
|
96
|
-
/** A friend request can be sent (both claimed, no relation yet or theirs is pending, requests on). */
|
|
97
|
-
canFriend: z.boolean(),
|
|
98
|
-
/** Friends who take messages, while DMs are on. */
|
|
99
|
-
canMessage: z.boolean(),
|
|
100
|
-
/** Friends who take messages. */
|
|
101
|
-
canInvite: z.boolean(),
|
|
102
|
-
/** Anyone but yourself. */
|
|
103
|
-
canReport: z.boolean(),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
/** A player card. */
|
|
107
|
-
export type PlayerCard = z.infer<typeof PlayerCard>;
|
|
108
|
-
|
|
109
|
-
/** Someone you recently played with (`GET /social/recent`). */
|
|
110
|
-
export const RecentPlayer = PlayerLite.extend({
|
|
111
|
-
/** The game you played together. */
|
|
112
|
-
game: Slug,
|
|
113
|
-
/** When you last played together (epoch ms). */
|
|
114
|
-
lastAt: Millis,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
/** A recent player. */
|
|
118
|
-
export type RecentPlayer = z.infer<typeof RecentPlayer>;
|
|
1
|
+
/**
|
|
2
|
+
* People shapes: your own social settings, friends, requests, player cards
|
|
3
|
+
* and recent players. Several are also part of the socket `hello` snapshot.
|
|
4
|
+
*
|
|
5
|
+
* Privacy rules behind them: a block in either direction looks to the
|
|
6
|
+
* blocked side like an ordinary unfriend (or a missing player), suspended
|
|
7
|
+
* players are invisible, and guests have no friends.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Millis, Slug } from '../../common';
|
|
11
|
+
import { PlayerLite, SessionUser } from '../../entities';
|
|
12
|
+
import { Presence } from './presence';
|
|
13
|
+
|
|
14
|
+
/** Who may see my presence, or message me: friends, or nobody. */
|
|
15
|
+
export const Audience = z.enum(['friends', 'nobody']);
|
|
16
|
+
|
|
17
|
+
/** An audience setting. */
|
|
18
|
+
export type Audience = z.infer<typeof Audience>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Your own account's social fields (`GET`/`PUT /social/me`, the `hello`
|
|
22
|
+
* snapshot's `me`). Never shown to other players.
|
|
23
|
+
*/
|
|
24
|
+
export const SocialMe = SessionUser.extend({
|
|
25
|
+
/** Avatar token; never empty (a default is derived from the id). */
|
|
26
|
+
avatar: PlayerLite.shape.avatar,
|
|
27
|
+
/** Who sees my presence and last-seen time. */
|
|
28
|
+
privacy: Audience,
|
|
29
|
+
/** Who may message me (and invite me). */
|
|
30
|
+
dmPolicy: Audience,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Your own social settings. */
|
|
34
|
+
export type SocialMe = z.infer<typeof SocialMe>;
|
|
35
|
+
|
|
36
|
+
/** A friend, with what you may see of their presence. Online friends sort first. */
|
|
37
|
+
export const FriendEntry = PlayerLite.extend({
|
|
38
|
+
/** `null` when offline or hiding their presence. */
|
|
39
|
+
presence: Presence.nullable(),
|
|
40
|
+
/** "Tell me when they come online" is on for this friend. */
|
|
41
|
+
notifyOnline: z.boolean(),
|
|
42
|
+
/** When you became friends (epoch ms). */
|
|
43
|
+
since: Millis,
|
|
44
|
+
/** When they were last online (epoch ms); `0` when unknown or they hide their presence. */
|
|
45
|
+
lastSeenAt: Millis,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** A friend. */
|
|
49
|
+
export type FriendEntry = z.infer<typeof FriendEntry>;
|
|
50
|
+
|
|
51
|
+
/** Pending friend requests both ways, newest first (blocked and suspended players left out). */
|
|
52
|
+
export const FriendRequests = z.object({
|
|
53
|
+
/** Requests waiting for you. */
|
|
54
|
+
incoming: z.array(PlayerLite),
|
|
55
|
+
/** Requests you sent. */
|
|
56
|
+
outgoing: z.array(PlayerLite),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Pending friend requests. */
|
|
60
|
+
export type FriendRequests = z.infer<typeof FriendRequests>;
|
|
61
|
+
|
|
62
|
+
/** `GET /social/friends`: friends, requests and the players you blocked. Guests get empty lists. */
|
|
63
|
+
export const FriendsList = FriendRequests.extend({
|
|
64
|
+
friends: z.array(FriendEntry),
|
|
65
|
+
/** Players you blocked, newest first. */
|
|
66
|
+
blocked: z.array(PlayerLite),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Friends, requests and blocks. */
|
|
70
|
+
export type FriendsList = z.infer<typeof FriendsList>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What changed in `ev:friends` (the client refetches `GET /social/friends`).
|
|
74
|
+
* `restored`: a friend's suspension ended. A player you blocked only ever
|
|
75
|
+
* sees `removed`.
|
|
76
|
+
*/
|
|
77
|
+
export const FriendChange = z.enum(['request', 'accepted', 'removed', 'declined', 'cancelled', 'blocked', 'restored']);
|
|
78
|
+
|
|
79
|
+
/** A friends-list change. */
|
|
80
|
+
export type FriendChange = z.infer<typeof FriendChange>;
|
|
81
|
+
|
|
82
|
+
/** My relation to another player, as their card shows it. */
|
|
83
|
+
export const Relation = z.enum(['self', 'friend', 'incoming', 'outgoing', 'blocked', 'none']);
|
|
84
|
+
|
|
85
|
+
/** A relation. */
|
|
86
|
+
export type Relation = z.infer<typeof Relation>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A player card (`GET /social/players/:id`). Someone who blocked you, or who
|
|
90
|
+
* is suspended, answers 404 exactly like a player who doesn't exist.
|
|
91
|
+
*/
|
|
92
|
+
export const PlayerCard = PlayerLite.extend({
|
|
93
|
+
relation: Relation,
|
|
94
|
+
/** Friends only; `null` otherwise, or when offline or hidden. */
|
|
95
|
+
presence: Presence.nullable(),
|
|
96
|
+
/** A friend request can be sent (both claimed, no relation yet or theirs is pending, requests on). */
|
|
97
|
+
canFriend: z.boolean(),
|
|
98
|
+
/** Friends who take messages, while DMs are on. */
|
|
99
|
+
canMessage: z.boolean(),
|
|
100
|
+
/** Friends who take messages. */
|
|
101
|
+
canInvite: z.boolean(),
|
|
102
|
+
/** Anyone but yourself. */
|
|
103
|
+
canReport: z.boolean(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** A player card. */
|
|
107
|
+
export type PlayerCard = z.infer<typeof PlayerCard>;
|
|
108
|
+
|
|
109
|
+
/** Someone you recently played with (`GET /social/recent`). */
|
|
110
|
+
export const RecentPlayer = PlayerLite.extend({
|
|
111
|
+
/** The game you played together. */
|
|
112
|
+
game: Slug,
|
|
113
|
+
/** When you last played together (epoch ms). */
|
|
114
|
+
lastAt: Millis,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** A recent player. */
|
|
118
|
+
export type RecentPlayer = z.infer<typeof RecentPlayer>;
|