@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
package/src/api/messages.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Direct messages between mutual friends (multiplayer-impl.md §5.2).
|
|
3
|
-
*
|
|
4
|
-
* "Checked before delivered": text is moderated before it is stored or
|
|
5
|
-
* pushed. Free text needs the conversation's chat mode to be `on` (both
|
|
6
|
-
* claimed, friends, no parent lock, site switch on); otherwise only
|
|
7
|
-
* quick-chat ids go through. Hidden messages and messages from currently
|
|
8
|
-
* suspended senders are left out of every read.
|
|
9
|
-
*/
|
|
10
|
-
import { z } from 'zod';
|
|
11
|
-
import { Millis, Ok, UserId } from '../common';
|
|
12
|
-
import { LooseNumber, requiredId } from '../coerce';
|
|
13
|
-
import { PlayerLite } from '../entities';
|
|
14
|
-
import { ContentRejected, ErrorBody } from '../errors';
|
|
15
|
-
import { requireClaimed } from '../middleware';
|
|
16
|
-
import { ChatMode } from '../entities';
|
|
17
|
-
import { QuickId } from './social/config';
|
|
18
|
-
|
|
19
|
-
/** A message id: a positive integer, increasing over time. */
|
|
20
|
-
export const MessageId = z.number().int().positive();
|
|
21
|
-
|
|
22
|
-
/** A direct message. */
|
|
23
|
-
export const Message = z.object({
|
|
24
|
-
id: MessageId,
|
|
25
|
-
/** The sender. */
|
|
26
|
-
from: UserId,
|
|
27
|
-
/** The recipient. */
|
|
28
|
-
to: UserId,
|
|
29
|
-
/** The delivered (possibly softened) text; a quick message carries its phrase's text. */
|
|
30
|
-
text: z.string(),
|
|
31
|
-
/** The quick-chat phrase id, for quick messages only. */
|
|
32
|
-
quick: QuickId.optional(),
|
|
33
|
-
/** When it was sent (epoch ms). */
|
|
34
|
-
at: Millis,
|
|
35
|
-
/** The recipient has read it. */
|
|
36
|
-
read: z.boolean(),
|
|
37
|
-
/** Words were swapped by the filter. Only on the sender's own copy. */
|
|
38
|
-
softened: z.literal(true).optional(),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/** A direct message. */
|
|
42
|
-
export type Message = z.infer<typeof Message>;
|
|
43
|
-
|
|
44
|
-
/** A conversation summary: the other player, the last message and how many of theirs you haven't read. */
|
|
45
|
-
export const Conversation = z.object({
|
|
46
|
-
user: PlayerLite,
|
|
47
|
-
last: Message,
|
|
48
|
-
unread: z.number().int().nonnegative(),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
/** A conversation summary. */
|
|
52
|
-
export type Conversation = z.infer<typeof Conversation>;
|
|
53
|
-
|
|
54
|
-
/** One page of a conversation's history. */
|
|
55
|
-
export const MessagesPage = z.object({
|
|
56
|
-
/** Oldest to newest, at most 50. */
|
|
57
|
-
messages: z.array(Message),
|
|
58
|
-
/** Older messages exist (page with `?before=` the first id). */
|
|
59
|
-
more: z.boolean(),
|
|
60
|
-
/** The mode the chat UI should render: `off` when DMs are off, `quick` while you are muted. */
|
|
61
|
-
mode: ChatMode,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** A page of history. */
|
|
65
|
-
export type MessagesPage = z.infer<typeof MessagesPage>;
|
|
66
|
-
|
|
67
|
-
/** A DM to send: free text, or a quick-chat phrase id (exactly one). */
|
|
68
|
-
export const SendMessageInput = z.object({
|
|
69
|
-
/** Free text (moderated; only when the conversation's mode is `on`). */
|
|
70
|
-
text: z.string().optional(),
|
|
71
|
-
/** A quick-chat phrase id (any mode but `off`). */
|
|
72
|
-
quick: QuickId.optional(),
|
|
73
|
-
}).refine((b) => (b.text === undefined) !== (b.quick === undefined), 'Send either text or a quick-chat id.');
|
|
74
|
-
|
|
75
|
-
/** A DM to send. */
|
|
76
|
-
export type SendMessageInput = z.infer<typeof SendMessageInput>;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Unread DMs (the `hello` snapshot's `unread`), ignoring hidden messages and
|
|
80
|
-
* suspended senders.
|
|
81
|
-
*/
|
|
82
|
-
export const UnreadSummary = z.object({
|
|
83
|
-
/** Total unread. */
|
|
84
|
-
dms: z.number().int().nonnegative(),
|
|
85
|
-
/** Unread per sender, keyed by the sender's user id (as a decimal string). */
|
|
86
|
-
byUser: z.record(z.string().regex(/^\d+$/), z.number().int().positive()),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
/** Unread DM counts. */
|
|
90
|
-
export type UnreadSummary = z.infer<typeof UnreadSummary>;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* `:userId`, the other side of the conversation: a claimed, non-suspended
|
|
94
|
-
* player other than you, else 404 "No such player." Read like `Number(x)`:
|
|
95
|
-
* anything that is not a player's id is the same 404.
|
|
96
|
-
*/
|
|
97
|
-
const Partner = z.object({ userId: LooseNumber.catch(Number.NaN) });
|
|
98
|
-
|
|
99
|
-
/** The direct-message endpoints. */
|
|
100
|
-
export const messagesEndpoints = {
|
|
101
|
-
...requireClaimed.group({
|
|
102
|
-
listConversations: {
|
|
103
|
-
method: 'GET', path: '/social/conversations',
|
|
104
|
-
response: z.object({
|
|
105
|
-
/** Newest first, one per partner, at most 200. */
|
|
106
|
-
conversations: z.array(Conversation),
|
|
107
|
-
}),
|
|
108
|
-
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
109
|
-
doc: { summary: 'Your conversations, newest first', tags: ['messages'] },
|
|
110
|
-
},
|
|
111
|
-
listDirectMessages: {
|
|
112
|
-
method: 'GET', path: '/social/messages/:userId', params: Partner,
|
|
113
|
-
query: z.object({
|
|
114
|
-
/** Only messages older than this message id; `0` or anything not a number means from the newest. */
|
|
115
|
-
before: LooseNumber.optional().catch(undefined),
|
|
116
|
-
}),
|
|
117
|
-
response: MessagesPage,
|
|
118
|
-
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
119
|
-
doc: { summary: 'One page of history with a player', tags: ['messages'] },
|
|
120
|
-
},
|
|
121
|
-
sendDirectMessage: {
|
|
122
|
-
method: 'POST', path: '/social/messages/:userId', params: Partner, body: SendMessageInput,
|
|
123
|
-
response: z.object({ message: Message }),
|
|
124
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 422: ContentRejected },
|
|
125
|
-
doc: {
|
|
126
|
-
summary: 'Send a DM to a friend',
|
|
127
|
-
description: '403 `not_friends` or `dms_closed`; 422 when the text is rejected, the chat mode forbids it, or you are sending too fast.',
|
|
128
|
-
tags: ['messages'],
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
markDirectMessagesRead: {
|
|
132
|
-
method: 'POST', path: '/social/messages/:userId/read', params: Partner,
|
|
133
|
-
body: z.object({
|
|
134
|
-
/** Mark their messages to you up to and including this id. */
|
|
135
|
-
upTo: requiredId('upTo must be a message id.'),
|
|
136
|
-
}),
|
|
137
|
-
response: Ok,
|
|
138
|
-
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
139
|
-
doc: { summary: "Mark a friend's messages to you as read", tags: ['messages'] },
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Direct messages between mutual friends (multiplayer-impl.md §5.2).
|
|
3
|
+
*
|
|
4
|
+
* "Checked before delivered": text is moderated before it is stored or
|
|
5
|
+
* pushed. Free text needs the conversation's chat mode to be `on` (both
|
|
6
|
+
* claimed, friends, no parent lock, site switch on); otherwise only
|
|
7
|
+
* quick-chat ids go through. Hidden messages and messages from currently
|
|
8
|
+
* suspended senders are left out of every read.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { Millis, Ok, UserId } from '../common';
|
|
12
|
+
import { LooseNumber, requiredId } from '../coerce';
|
|
13
|
+
import { PlayerLite } from '../entities';
|
|
14
|
+
import { ContentRejected, ErrorBody } from '../errors';
|
|
15
|
+
import { requireClaimed } from '../middleware';
|
|
16
|
+
import { ChatMode } from '../entities';
|
|
17
|
+
import { QuickId } from './social/config';
|
|
18
|
+
|
|
19
|
+
/** A message id: a positive integer, increasing over time. */
|
|
20
|
+
export const MessageId = z.number().int().positive();
|
|
21
|
+
|
|
22
|
+
/** A direct message. */
|
|
23
|
+
export const Message = z.object({
|
|
24
|
+
id: MessageId,
|
|
25
|
+
/** The sender. */
|
|
26
|
+
from: UserId,
|
|
27
|
+
/** The recipient. */
|
|
28
|
+
to: UserId,
|
|
29
|
+
/** The delivered (possibly softened) text; a quick message carries its phrase's text. */
|
|
30
|
+
text: z.string(),
|
|
31
|
+
/** The quick-chat phrase id, for quick messages only. */
|
|
32
|
+
quick: QuickId.optional(),
|
|
33
|
+
/** When it was sent (epoch ms). */
|
|
34
|
+
at: Millis,
|
|
35
|
+
/** The recipient has read it. */
|
|
36
|
+
read: z.boolean(),
|
|
37
|
+
/** Words were swapped by the filter. Only on the sender's own copy. */
|
|
38
|
+
softened: z.literal(true).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** A direct message. */
|
|
42
|
+
export type Message = z.infer<typeof Message>;
|
|
43
|
+
|
|
44
|
+
/** A conversation summary: the other player, the last message and how many of theirs you haven't read. */
|
|
45
|
+
export const Conversation = z.object({
|
|
46
|
+
user: PlayerLite,
|
|
47
|
+
last: Message,
|
|
48
|
+
unread: z.number().int().nonnegative(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** A conversation summary. */
|
|
52
|
+
export type Conversation = z.infer<typeof Conversation>;
|
|
53
|
+
|
|
54
|
+
/** One page of a conversation's history. */
|
|
55
|
+
export const MessagesPage = z.object({
|
|
56
|
+
/** Oldest to newest, at most 50. */
|
|
57
|
+
messages: z.array(Message),
|
|
58
|
+
/** Older messages exist (page with `?before=` the first id). */
|
|
59
|
+
more: z.boolean(),
|
|
60
|
+
/** The mode the chat UI should render: `off` when DMs are off, `quick` while you are muted. */
|
|
61
|
+
mode: ChatMode,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** A page of history. */
|
|
65
|
+
export type MessagesPage = z.infer<typeof MessagesPage>;
|
|
66
|
+
|
|
67
|
+
/** A DM to send: free text, or a quick-chat phrase id (exactly one). */
|
|
68
|
+
export const SendMessageInput = z.object({
|
|
69
|
+
/** Free text (moderated; only when the conversation's mode is `on`). */
|
|
70
|
+
text: z.string().optional(),
|
|
71
|
+
/** A quick-chat phrase id (any mode but `off`). */
|
|
72
|
+
quick: QuickId.optional(),
|
|
73
|
+
}).refine((b) => (b.text === undefined) !== (b.quick === undefined), 'Send either text or a quick-chat id.');
|
|
74
|
+
|
|
75
|
+
/** A DM to send. */
|
|
76
|
+
export type SendMessageInput = z.infer<typeof SendMessageInput>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Unread DMs (the `hello` snapshot's `unread`), ignoring hidden messages and
|
|
80
|
+
* suspended senders.
|
|
81
|
+
*/
|
|
82
|
+
export const UnreadSummary = z.object({
|
|
83
|
+
/** Total unread. */
|
|
84
|
+
dms: z.number().int().nonnegative(),
|
|
85
|
+
/** Unread per sender, keyed by the sender's user id (as a decimal string). */
|
|
86
|
+
byUser: z.record(z.string().regex(/^\d+$/), z.number().int().positive()),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** Unread DM counts. */
|
|
90
|
+
export type UnreadSummary = z.infer<typeof UnreadSummary>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `:userId`, the other side of the conversation: a claimed, non-suspended
|
|
94
|
+
* player other than you, else 404 "No such player." Read like `Number(x)`:
|
|
95
|
+
* anything that is not a player's id is the same 404.
|
|
96
|
+
*/
|
|
97
|
+
const Partner = z.object({ userId: LooseNumber.catch(Number.NaN) });
|
|
98
|
+
|
|
99
|
+
/** The direct-message endpoints. */
|
|
100
|
+
export const messagesEndpoints = {
|
|
101
|
+
...requireClaimed.group({
|
|
102
|
+
listConversations: {
|
|
103
|
+
method: 'GET', path: '/social/conversations',
|
|
104
|
+
response: z.object({
|
|
105
|
+
/** Newest first, one per partner, at most 200. */
|
|
106
|
+
conversations: z.array(Conversation),
|
|
107
|
+
}),
|
|
108
|
+
errors: { 401: ErrorBody, 403: ErrorBody },
|
|
109
|
+
doc: { summary: 'Your conversations, newest first', tags: ['messages'] },
|
|
110
|
+
},
|
|
111
|
+
listDirectMessages: {
|
|
112
|
+
method: 'GET', path: '/social/messages/:userId', params: Partner,
|
|
113
|
+
query: z.object({
|
|
114
|
+
/** Only messages older than this message id; `0` or anything not a number means from the newest. */
|
|
115
|
+
before: LooseNumber.optional().catch(undefined),
|
|
116
|
+
}),
|
|
117
|
+
response: MessagesPage,
|
|
118
|
+
errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
119
|
+
doc: { summary: 'One page of history with a player', tags: ['messages'] },
|
|
120
|
+
},
|
|
121
|
+
sendDirectMessage: {
|
|
122
|
+
method: 'POST', path: '/social/messages/:userId', params: Partner, body: SendMessageInput,
|
|
123
|
+
response: z.object({ message: Message }),
|
|
124
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 422: ContentRejected },
|
|
125
|
+
doc: {
|
|
126
|
+
summary: 'Send a DM to a friend',
|
|
127
|
+
description: '403 `not_friends` or `dms_closed`; 422 when the text is rejected, the chat mode forbids it, or you are sending too fast.',
|
|
128
|
+
tags: ['messages'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
markDirectMessagesRead: {
|
|
132
|
+
method: 'POST', path: '/social/messages/:userId/read', params: Partner,
|
|
133
|
+
body: z.object({
|
|
134
|
+
/** Mark their messages to you up to and including this id. */
|
|
135
|
+
upTo: requiredId('upTo must be a message id.'),
|
|
136
|
+
}),
|
|
137
|
+
response: Ok,
|
|
138
|
+
errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
|
|
139
|
+
doc: { summary: "Mark a friend's messages to you as read", tags: ['messages'] },
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
};
|
package/src/api/mp/chat.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Group chat in hub rooms and parties (multiplayer.md §4.3, §4.8): what a
|
|
3
|
-
* player sends, and the moderated line everyone receives.
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { UserId } from '../../common';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A chat send: a quick-chat phrase id, or free text. When `quick` is set it
|
|
11
|
-
* wins and `text` is ignored.
|
|
12
|
-
*/
|
|
13
|
-
export const ChatSend = z.object({
|
|
14
|
-
/** Free text; only allowed in mode `on`, then moderated. */
|
|
15
|
-
text: z.string().optional(),
|
|
16
|
-
/** A quick-chat phrase id (hub phrase, `e:<emoji>` emote or game phrase). */
|
|
17
|
-
quick: z.string().optional(),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/** The line as delivered: the (possibly softened) text, and the phrase id for quick-chat. */
|
|
21
|
-
export const ChatLine = z.object({
|
|
22
|
-
text: z.string(),
|
|
23
|
-
/** Set when the line is a quick-chat phrase. */
|
|
24
|
-
quick: z.string().optional(),
|
|
25
|
-
/** Moderation reworded the text. */
|
|
26
|
-
softened: z.literal(true).optional(),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
/** A delivered chat line. */
|
|
30
|
-
export type ChatLine = z.infer<typeof ChatLine>;
|
|
31
|
-
|
|
32
|
-
/** A chat line pushed to a room or party, with its sender. */
|
|
33
|
-
export const ChatLineEvent = ChatLine.extend({ from: UserId });
|
|
34
|
-
|
|
35
|
-
/** The error codes a chat send can fail with (besides its surface's own). */
|
|
36
|
-
export const CHAT_ERRORS = ['rate_limited', 'content_rejected', 'bad_quick'] as const;
|
|
1
|
+
/**
|
|
2
|
+
* Group chat in hub rooms and parties (multiplayer.md §4.3, §4.8): what a
|
|
3
|
+
* player sends, and the moderated line everyone receives.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { UserId } from '../../common';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A chat send: a quick-chat phrase id, or free text. When `quick` is set it
|
|
11
|
+
* wins and `text` is ignored.
|
|
12
|
+
*/
|
|
13
|
+
export const ChatSend = z.object({
|
|
14
|
+
/** Free text; only allowed in mode `on`, then moderated. */
|
|
15
|
+
text: z.string().optional(),
|
|
16
|
+
/** A quick-chat phrase id (hub phrase, `e:<emoji>` emote or game phrase). */
|
|
17
|
+
quick: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** The line as delivered: the (possibly softened) text, and the phrase id for quick-chat. */
|
|
21
|
+
export const ChatLine = z.object({
|
|
22
|
+
text: z.string(),
|
|
23
|
+
/** Set when the line is a quick-chat phrase. */
|
|
24
|
+
quick: z.string().optional(),
|
|
25
|
+
/** Moderation reworded the text. */
|
|
26
|
+
softened: z.literal(true).optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** A delivered chat line. */
|
|
30
|
+
export type ChatLine = z.infer<typeof ChatLine>;
|
|
31
|
+
|
|
32
|
+
/** A chat line pushed to a room or party, with its sender. */
|
|
33
|
+
export const ChatLineEvent = ChatLine.extend({ from: UserId });
|
|
34
|
+
|
|
35
|
+
/** The error codes a chat send can fail with (besides its surface's own). */
|
|
36
|
+
export const CHAT_ERRORS = ['rate_limited', 'content_rejected', 'bad_quick'] as const;
|