@omelhorsite/sdk 0.2.0 → 0.4.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/README.md +112 -130
- package/dist/index.js +4800 -610
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +5 -1
- package/dist/types/internal/attachments.d.ts +17 -0
- package/dist/types/internal/helpers.d.ts +9 -0
- package/dist/types/listing.d.ts +90 -0
- package/dist/types/resources/account.d.ts +71 -5
- package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
- package/dist/types/resources/admin/chests.d.ts +51 -0
- package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
- package/dist/types/resources/admin/identities.d.ts +99 -0
- package/dist/types/resources/admin/index.d.ts +77 -0
- package/dist/types/resources/admin/jobs.d.ts +97 -0
- package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
- package/dist/types/resources/admin/notepads.d.ts +49 -0
- package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
- package/dist/types/resources/admin/quotas.d.ts +114 -0
- package/dist/types/resources/admin/shortLinks.d.ts +201 -0
- package/dist/types/resources/admin/types.d.ts +222 -0
- package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
- package/dist/types/resources/admin.d.ts +1841 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +844 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content/analysis.d.ts +62 -0
- package/dist/types/resources/content/blogs.d.ts +471 -0
- package/dist/types/resources/content/feedbacks.d.ts +263 -0
- package/dist/types/resources/content/index.d.ts +52 -0
- package/dist/types/resources/content/intel/articles.d.ts +230 -0
- package/dist/types/resources/content/intel/config.d.ts +135 -0
- package/dist/types/resources/content/intel/index.d.ts +53 -0
- package/dist/types/resources/content/intel/items.d.ts +91 -0
- package/dist/types/resources/content/intel/reports.d.ts +108 -0
- package/dist/types/resources/content/intel/scripts.d.ts +152 -0
- package/dist/types/resources/content/intel/sources.d.ts +236 -0
- package/dist/types/resources/content/intel/stats.d.ts +105 -0
- package/dist/types/resources/content/intel/types.d.ts +86 -0
- package/dist/types/resources/content/jokes.d.ts +93 -0
- package/dist/types/resources/content/notifications.d.ts +147 -0
- package/dist/types/resources/content/serviceUsages.d.ts +73 -0
- package/dist/types/resources/content/servicesStatus.d.ts +194 -0
- package/dist/types/resources/content/siteConfig.d.ts +43 -0
- package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
- package/dist/types/resources/content.d.ts +2775 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +69 -25
- package/dist/types/resources/library/annotations.d.ts +162 -0
- package/dist/types/resources/library/books.d.ts +534 -0
- package/dist/types/resources/library/chat.d.ts +259 -0
- package/dist/types/resources/library/index.d.ts +39 -0
- package/dist/types/resources/library/shelves.d.ts +237 -0
- package/dist/types/resources/library/types.d.ts +120 -0
- package/dist/types/resources/library.d.ts +1433 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies/addons.d.ts +388 -0
- package/dist/types/resources/movies/collections.d.ts +352 -0
- package/dist/types/resources/movies/index.d.ts +29 -0
- package/dist/types/resources/movies/types.d.ts +50 -0
- package/dist/types/resources/movies/watchProgress.d.ts +277 -0
- package/dist/types/resources/movies.d.ts +1160 -0
- package/dist/types/resources/music/artists.d.ts +1068 -0
- package/dist/types/resources/music/imports.d.ts +941 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1027 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1167 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +49 -5
- package/dist/types/resources/social/groupChats.d.ts +458 -0
- package/dist/types/resources/social/index.d.ts +19 -0
- package/dist/types/resources/social/messages.d.ts +424 -0
- package/dist/types/resources/social/relationships.d.ts +322 -0
- package/dist/types/resources/social/types.d.ts +37 -0
- package/dist/types/resources/social.d.ts +1348 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +111 -27
- package/dist/types/resources/tickets.d.ts +101 -13
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- package/package.json +2 -1
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `social` namespace: direct messages, relationships and group chats.
|
|
3
|
+
*
|
|
4
|
+
* Three tables' worth of API that a chat screen needs at once, exposed as one
|
|
5
|
+
* entry class with three sub-namespaces
|
|
6
|
+
* ({@link SocialNamespace.messages}, `.relationships`, `.groupChats`) plus a
|
|
7
|
+
* fourth hanging off the third ({@link GroupChatsNamespace.messages}). Every
|
|
8
|
+
* sub-namespace is exported on its own so a host that prefers
|
|
9
|
+
* `oms.messages` can mount it there instead.
|
|
10
|
+
*
|
|
11
|
+
* ## THIS NAMESPACE IS HALF A CHAT CLIENT. THE OTHER HALF IS THE CABLE.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is HTTP request/response, and HTTP request/response cannot
|
|
14
|
+
* deliver a message you did not ask for. A UI built on this file alone shows a
|
|
15
|
+
* new message when it next polls, which is the difference between a chat app
|
|
16
|
+
* and a mailbox with a refresh button.
|
|
17
|
+
*
|
|
18
|
+
* The live half such as it is arrives over Action Cable (`/cable`), on
|
|
19
|
+
* `NotificationsChannel` - one stream per user, which this SDK does not speak.
|
|
20
|
+
* Nothing in this file opens a socket, and no method here will ever resolve
|
|
21
|
+
* because somebody else typed.
|
|
22
|
+
*
|
|
23
|
+
* And the cable carries LESS than a chat client wants. What lands is
|
|
24
|
+
* `{ type: "created", notification, unread_count }`, where `notification` is a
|
|
25
|
+
* `Notification` row, not a message: for a direct message its `kind` is
|
|
26
|
+
* `"message_received"` and its `context` is
|
|
27
|
+
* `{ sender_id, message_id, preview }` - 120 characters of text and an id. So
|
|
28
|
+
* even a client that speaks the cable has to come back here for the record.
|
|
29
|
+
* Relationships get `friendship_request` and `friendship_accepted` the same
|
|
30
|
+
* way. **Group chats get nothing at all** - no channel, no notification row,
|
|
31
|
+
* no push - and must be polled; see {@link GroupChatsNamespace}.
|
|
32
|
+
*
|
|
33
|
+
* So the shape a real client takes is: this namespace for history, sending,
|
|
34
|
+
* editing and the conversation list; the cable as a nudge to re-fetch; and a
|
|
35
|
+
* reconcile on reconnect, because a socket that was down missed messages that
|
|
36
|
+
* only {@link DirectMessagesNamespace.list} and
|
|
37
|
+
* {@link GroupChatMessagesNamespace.list} can fill back in. Until the SDK grows
|
|
38
|
+
* a cable transport, all of that is the host's problem.
|
|
39
|
+
*
|
|
40
|
+
* ## IDS ARE INCONSISTENT ACROSS THE THREE FAMILIES
|
|
41
|
+
*
|
|
42
|
+
* `messages` and `relationships` kept auto-increment primary keys, so their
|
|
43
|
+
* ids arrive as JSON NUMBERS. `group_chats`, `group_chat_members` and
|
|
44
|
+
* `group_chat_messages` are `id: :string` tables, so theirs arrive as STRINGS.
|
|
45
|
+
* The `sender_id` / `receiver_id` / `user_id` on all five are strings, because
|
|
46
|
+
* users are. That is why {@link DirectMessage} and {@link Relationship} spell
|
|
47
|
+
* their `id` out rather than extending `BaseRecord`, whose `id` is a string,
|
|
48
|
+
* while {@link GroupChat} and {@link GroupChatMessage} do extend it.
|
|
49
|
+
*
|
|
50
|
+
* ## Reachability
|
|
51
|
+
*
|
|
52
|
+
* None of these routes is anonymous, and none of them declares an
|
|
53
|
+
* `oauth_scope`. `Authentication#enforce_oauth_scope!` denies by default, so a
|
|
54
|
+
* CLI or MCP host holding a Doorkeeper access token gets
|
|
55
|
+
* `403 {"error":"insufficient_scope"}` on every method in this file whatever
|
|
56
|
+
* scopes it was granted. Use a session token (`POST /sessions`) or, in the
|
|
57
|
+
* browser, the session cookie.
|
|
58
|
+
*
|
|
59
|
+
* ## Throttling
|
|
60
|
+
*
|
|
61
|
+
* `Rack::Attack` has no bucket of its own for any of these paths: they all
|
|
62
|
+
* land in `general/authed`, **600 requests a minute** keyed on the
|
|
63
|
+
* `Authorization` header (or `general/anon`, 120/min/IP, which none of these
|
|
64
|
+
* routes can reach because they all require a user). That ceiling is shared
|
|
65
|
+
* with every other authenticated call the same client makes, so a chat screen
|
|
66
|
+
* polling three endpoints on a two-second timer is spending 90/min of a budget
|
|
67
|
+
* the rest of the app also draws on. See the per-method notes for where the
|
|
68
|
+
* cost actually is.
|
|
69
|
+
*/
|
|
70
|
+
import { Resource } from "../http";
|
|
71
|
+
import type { ListParams } from "../listing";
|
|
72
|
+
import type { User } from "./account";
|
|
73
|
+
import type { BaseRecord, FileInput, Id, NativeFile, Paginated, RequestOptions, Timestamp } from "../types";
|
|
74
|
+
/**
|
|
75
|
+
* Primary key of a direct message. An INTEGER: `messages` is one of the tables
|
|
76
|
+
* that kept an auto-increment key, so `id` and `quoted_message_id` are JSON
|
|
77
|
+
* numbers while `sender_id` and `receiver_id` beside them are strings.
|
|
78
|
+
*/
|
|
79
|
+
export type MessageId = number;
|
|
80
|
+
/** Primary key of a relationship row. An integer, for the same reason. */
|
|
81
|
+
export type RelationshipId = number;
|
|
82
|
+
/**
|
|
83
|
+
* Primary key of a group chat. A STRING - `group_chats` is an `id: :string`
|
|
84
|
+
* table, unlike the two above it in this same file.
|
|
85
|
+
*/
|
|
86
|
+
export type GroupChatId = string;
|
|
87
|
+
/** Primary key of a membership row. A string. */
|
|
88
|
+
export type GroupChatMemberId = string;
|
|
89
|
+
/** Primary key of a group chat message. A string. */
|
|
90
|
+
export type GroupChatMessageId = string;
|
|
91
|
+
/** `Message` validates `content` at 1024 characters. Longer is a `400`. */
|
|
92
|
+
export declare const MESSAGE_CONTENT_MAX_LENGTH = 1024;
|
|
93
|
+
/** `GroupChatMessage` allows four thousand, not one thousand. */
|
|
94
|
+
export declare const GROUP_CHAT_MESSAGE_CONTENT_MAX_LENGTH = 4000;
|
|
95
|
+
/** `GroupChat.NAME_MAX`. */
|
|
96
|
+
export declare const GROUP_CHAT_NAME_MAX_LENGTH = 120;
|
|
97
|
+
/**
|
|
98
|
+
* `ATTACHMENT_MAX_BYTES` on both message models: 25 MiB, checked in the
|
|
99
|
+
* controller before anything is attached.
|
|
100
|
+
*
|
|
101
|
+
* IMAGES HAVE A LOWER, WORSE-BEHAVED CEILING. See
|
|
102
|
+
* {@link MESSAGE_IMAGE_MAX_BYTES}.
|
|
103
|
+
*/
|
|
104
|
+
export declare const MESSAGE_ATTACHMENT_MAX_BYTES: number;
|
|
105
|
+
/**
|
|
106
|
+
* The ceiling that actually applies to an attachment the server decides is an
|
|
107
|
+
* image: `ImageProcessors::Message::MAX_SIZE`, 20 MiB.
|
|
108
|
+
*
|
|
109
|
+
* The two caps disagree and the gap is not harmless. The controller checks 25
|
|
110
|
+
* MiB, then hands anything whose `content_type` starts with `image/` to the
|
|
111
|
+
* webp recompressor, which checks 20 MiB and raises a plain `ArgumentError` -
|
|
112
|
+
* not the `InvalidImage` that `ApplicationController` turns into a `400`. So a
|
|
113
|
+
* 22 MiB JPEG is a **500** with a Discord error alert behind it, where a 22 MiB
|
|
114
|
+
* zip is accepted.
|
|
115
|
+
*
|
|
116
|
+
* {@link DirectMessagesNamespace.send} refuses that band client-side when the
|
|
117
|
+
* size is knowable, which is the only place the mistake is still cheap.
|
|
118
|
+
*/
|
|
119
|
+
export declare const MESSAGE_IMAGE_MAX_BYTES: number;
|
|
120
|
+
/**
|
|
121
|
+
* Pixel ceilings `ImageProcessors::Processor#reject_bomb!` applies to an image
|
|
122
|
+
* attachment before any full-frame decode: 12000px on either side, and 50
|
|
123
|
+
* megapixels of area. Past either one the answer is `400` with the dimensions
|
|
124
|
+
* in the body, and deliberately no error alert.
|
|
125
|
+
*
|
|
126
|
+
* Not checkable here - the SDK does not decode images - so this is
|
|
127
|
+
* documentation, not a guard.
|
|
128
|
+
*/
|
|
129
|
+
export declare const MESSAGE_IMAGE_MAX_DIMENSION_PX = 12000;
|
|
130
|
+
/** @see MESSAGE_IMAGE_MAX_DIMENSION_PX */
|
|
131
|
+
export declare const MESSAGE_IMAGE_MAX_PIXELS = 50000000;
|
|
132
|
+
/**
|
|
133
|
+
* `EDIT_WINDOW` on both message models: fifteen minutes from `created_at`,
|
|
134
|
+
* after which `updatable_by?` is false and an edit is `401`.
|
|
135
|
+
*
|
|
136
|
+
* Measured against the SERVER's clock. {@link canEditMessage} compares against
|
|
137
|
+
* the caller's, which is close enough to grey out a button and not close
|
|
138
|
+
* enough to promise the edit will land.
|
|
139
|
+
*/
|
|
140
|
+
export declare const MESSAGE_EDIT_WINDOW_MS: number;
|
|
141
|
+
/**
|
|
142
|
+
* Rows `GET /group_chats/:id/messages` returns per call.
|
|
143
|
+
*
|
|
144
|
+
* A hard constant in `GroupChatMessagesController`, not a modifier: this
|
|
145
|
+
* endpoint does not speak the list DSL and `modifiers[page]` on it is ignored.
|
|
146
|
+
*/
|
|
147
|
+
export declare const GROUP_CHAT_MESSAGE_PAGE_SIZE = 100;
|
|
148
|
+
/** How the server classified an attachment, from its `content_type`. */
|
|
149
|
+
export type AttachmentKind = "image" | "audio" | "video" | "file";
|
|
150
|
+
/** Preview of the message a message quotes. Truncated to 120 characters. */
|
|
151
|
+
export interface QuotedMessagePreview {
|
|
152
|
+
readonly id: MessageId;
|
|
153
|
+
readonly sender_id: Id;
|
|
154
|
+
readonly content: string;
|
|
155
|
+
readonly attachment_kind: AttachmentKind | null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A direct message between two users.
|
|
159
|
+
*
|
|
160
|
+
* Every row carries BOTH ends of the conversation as fully rendered
|
|
161
|
+
* {@link User} objects, from `association :sender` / `:receiver` on the
|
|
162
|
+
* blueprint's default view. That is most of the bytes of a page of messages,
|
|
163
|
+
* it is the same two records repeated a hundred times, and there is no view
|
|
164
|
+
* that omits them. Budget for it on a mobile connection.
|
|
165
|
+
*/
|
|
166
|
+
export interface DirectMessage {
|
|
167
|
+
/** Integer. */
|
|
168
|
+
readonly id: MessageId;
|
|
169
|
+
readonly created_at: Timestamp;
|
|
170
|
+
readonly updated_at: Timestamp;
|
|
171
|
+
/** Never null and never empty. */
|
|
172
|
+
readonly content: string;
|
|
173
|
+
readonly sender_id: Id;
|
|
174
|
+
readonly receiver_id: Id;
|
|
175
|
+
/**
|
|
176
|
+
* DO NOT READ THIS AS "THE OTHER PERSON SAW IT".
|
|
177
|
+
*
|
|
178
|
+
* It is set by `MessagesController#index` on every row the listing matched,
|
|
179
|
+
* in both directions, so it means "somebody who can see this message listed
|
|
180
|
+
* it" and nothing more precise. On a message the CALLER sent, it was almost
|
|
181
|
+
* certainly stamped by the caller's own listing rather than by the
|
|
182
|
+
* recipient's - opening a thread marks your own outgoing messages read. A
|
|
183
|
+
* "seen" tick built on this field lies.
|
|
184
|
+
*
|
|
185
|
+
* On a message the caller RECEIVED it is meaningful, in the weak sense that
|
|
186
|
+
* their own client fetched it at that time. It is what
|
|
187
|
+
* {@link DirectMessagesNamespace.conversedUsers} counts, and the only
|
|
188
|
+
* unread signal the API has.
|
|
189
|
+
*/
|
|
190
|
+
readonly read_at: Timestamp | null;
|
|
191
|
+
readonly attachment_kind: AttachmentKind | null;
|
|
192
|
+
readonly quoted_message_id: MessageId | null;
|
|
193
|
+
/** Null until the sender edits it. */
|
|
194
|
+
readonly edited_at: Timestamp | null;
|
|
195
|
+
readonly has_image: boolean;
|
|
196
|
+
readonly has_attachment: boolean;
|
|
197
|
+
readonly attachment_filename: string | null;
|
|
198
|
+
readonly attachment_byte_size: number | null;
|
|
199
|
+
readonly quoted_preview: QuotedMessagePreview | null;
|
|
200
|
+
/** The full sender record, nested. */
|
|
201
|
+
readonly sender: User;
|
|
202
|
+
/** The full receiver record, nested. */
|
|
203
|
+
readonly receiver: User;
|
|
204
|
+
}
|
|
205
|
+
/** A relationship row: one friendship or one block, between two users. */
|
|
206
|
+
export interface Relationship {
|
|
207
|
+
/** Integer. */
|
|
208
|
+
readonly id: RelationshipId;
|
|
209
|
+
readonly created_at: Timestamp;
|
|
210
|
+
readonly updated_at: Timestamp;
|
|
211
|
+
readonly kind: RelationshipKind;
|
|
212
|
+
readonly status: RelationshipStatus;
|
|
213
|
+
/** Whoever created the row. Always `Current.user` at create time. */
|
|
214
|
+
readonly requester_id: Id;
|
|
215
|
+
readonly accepter_id: Id;
|
|
216
|
+
readonly requester: User;
|
|
217
|
+
readonly accepter: User;
|
|
218
|
+
}
|
|
219
|
+
/** One row of {@link DirectMessagesNamespace.conversedUsers}. */
|
|
220
|
+
export interface ConversedUser extends User {
|
|
221
|
+
readonly last_message: {
|
|
222
|
+
readonly id: MessageId;
|
|
223
|
+
readonly preview: string;
|
|
224
|
+
readonly from_me: boolean;
|
|
225
|
+
readonly created_at: Timestamp;
|
|
226
|
+
} | null;
|
|
227
|
+
readonly unread_count: number;
|
|
228
|
+
}
|
|
229
|
+
/** Newest message in a chat, as the chat record summarises it. */
|
|
230
|
+
export interface GroupChatLastMessage {
|
|
231
|
+
readonly id: GroupChatMessageId;
|
|
232
|
+
readonly sender_id: Id | null;
|
|
233
|
+
readonly preview: string;
|
|
234
|
+
readonly attachment_kind: AttachmentKind | null;
|
|
235
|
+
readonly created_at: Timestamp;
|
|
236
|
+
}
|
|
237
|
+
/** One membership row, as the `:extended` chat view embeds it. */
|
|
238
|
+
export interface GroupChatMember {
|
|
239
|
+
readonly id: GroupChatMemberId;
|
|
240
|
+
readonly user_id: Id;
|
|
241
|
+
readonly user_handle: string | null;
|
|
242
|
+
readonly user_name: string | null;
|
|
243
|
+
readonly role: GroupChatRole;
|
|
244
|
+
readonly joined_at: Timestamp;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* A group chat, as the index renders it.
|
|
248
|
+
*
|
|
249
|
+
* Extends {@link BaseRecord}, which the two integer-keyed records in this file
|
|
250
|
+
* cannot: `group_chats` is an `id: :string` table, so its key is the same
|
|
251
|
+
* opaque string every other resource in the SDK uses.
|
|
252
|
+
*/
|
|
253
|
+
export interface GroupChat extends BaseRecord {
|
|
254
|
+
readonly id: GroupChatId;
|
|
255
|
+
readonly name: string | null;
|
|
256
|
+
readonly kind: "ad_hoc";
|
|
257
|
+
readonly system_managed: boolean;
|
|
258
|
+
readonly context_type: string | null;
|
|
259
|
+
readonly context_id: string | null;
|
|
260
|
+
readonly last_message: GroupChatLastMessage | null;
|
|
261
|
+
}
|
|
262
|
+
/** The `:extended` view: {@link GroupChat} plus the roster. */
|
|
263
|
+
export interface GroupChatDetail extends GroupChat {
|
|
264
|
+
readonly members: GroupChatMember[];
|
|
265
|
+
}
|
|
266
|
+
/** A message inside a group chat. */
|
|
267
|
+
export interface GroupChatMessage extends BaseRecord {
|
|
268
|
+
readonly id: GroupChatMessageId;
|
|
269
|
+
readonly group_chat_id: GroupChatId;
|
|
270
|
+
/** Null on a system message. */
|
|
271
|
+
readonly sender_id: Id | null;
|
|
272
|
+
readonly sender_handle: string | null;
|
|
273
|
+
readonly sender_name: string | null;
|
|
274
|
+
readonly content: string | null;
|
|
275
|
+
readonly attachment_kind: AttachmentKind | null;
|
|
276
|
+
readonly has_attachment: boolean;
|
|
277
|
+
readonly system_kind: string | null;
|
|
278
|
+
readonly system_payload: Record<string, unknown>;
|
|
279
|
+
readonly edited_at: Timestamp | null;
|
|
280
|
+
readonly attachment_filename?: string;
|
|
281
|
+
readonly attachment_byte_size?: number;
|
|
282
|
+
readonly attachment_content_type?: string;
|
|
283
|
+
}
|
|
284
|
+
/** Membership role. */
|
|
285
|
+
export type GroupChatRole = "member" | "admin";
|
|
286
|
+
/** What a relationship row means. */
|
|
287
|
+
export type RelationshipKind = "friend" | "block";
|
|
288
|
+
/** Where a relationship row is in its lifecycle. */
|
|
289
|
+
export type RelationshipStatus = "pending" | "accepted";
|
|
290
|
+
/** The `social` namespace, reachable as `oms.social`. */
|
|
291
|
+
export declare class SocialNamespace extends Resource {
|
|
292
|
+
/** One-to-one messages. */
|
|
293
|
+
readonly messages: DirectMessagesNamespace;
|
|
294
|
+
/** Friendships and blocks. */
|
|
295
|
+
readonly relationships: RelationshipsNamespace;
|
|
296
|
+
/** Many-to-many chats, their roster and their messages. */
|
|
297
|
+
readonly groupChats: GroupChatsNamespace;
|
|
298
|
+
constructor(http: ConstructorParameters<typeof Resource>[0]);
|
|
299
|
+
}
|
|
300
|
+
/** Filter columns of `GET /messages`. */
|
|
301
|
+
export declare const MESSAGE_FILTER_COLUMNS: readonly ["id", "created_at", "updated_at", "content", "sender_id", "receiver_id", "read_at"];
|
|
302
|
+
/** `extra_options` keys of `GET /messages`. */
|
|
303
|
+
export declare const MESSAGE_EXTRA_OPTION_KEYS: readonly ["user_id", "other_user_id"];
|
|
304
|
+
/** `extra_options` of `GET /messages`. Each matches rows where the user is either end. */
|
|
305
|
+
export interface MessageExtraOptions {
|
|
306
|
+
readonly user_id?: Id;
|
|
307
|
+
readonly other_user_id?: Id;
|
|
308
|
+
}
|
|
309
|
+
/** Filters accepted by {@link DirectMessagesNamespace.list}. */
|
|
310
|
+
export interface ListMessagesParams extends ListParams<(typeof MESSAGE_FILTER_COLUMNS)[number], MessageExtraOptions> {
|
|
311
|
+
/**
|
|
312
|
+
* The other end of the conversation. Sent as
|
|
313
|
+
* `extra_options[other_user_id]`, which the server expands to
|
|
314
|
+
* `sender_id = ? OR receiver_id = ?` INSIDE the caller's own scope, so it
|
|
315
|
+
* yields exactly the thread with that person.
|
|
316
|
+
*
|
|
317
|
+
* Pass this on every call you can. It is the only thing that keeps
|
|
318
|
+
* {@link DirectMessagesNamespace.list}'s read-marking side effect from
|
|
319
|
+
* covering your whole mailbox - see the method's docs.
|
|
320
|
+
*/
|
|
321
|
+
readonly withUser?: Id;
|
|
322
|
+
/**
|
|
323
|
+
* `extra_options[user_id]`: rows where this user is EITHER end.
|
|
324
|
+
*
|
|
325
|
+
* Redundant for a normal caller - the listing scope is already
|
|
326
|
+
* `Current.user.messages`, so passing your own id narrows nothing - and it
|
|
327
|
+
* cannot widen the scope to somebody else's mailbox either. The web frontend
|
|
328
|
+
* sends it anyway (`Message.listWithUser` fetches the session just to fill it
|
|
329
|
+
* in), which costs a `GET /sessions/mine` per conversation open for no
|
|
330
|
+
* result. It is exposed here only so a caller porting that code can keep the
|
|
331
|
+
* wire identical.
|
|
332
|
+
*/
|
|
333
|
+
readonly userId?: Id;
|
|
334
|
+
/** `search[content]`: partial, case- and accent-insensitive. */
|
|
335
|
+
readonly contentContains?: string;
|
|
336
|
+
/** `exact_search[sender_id]`. */
|
|
337
|
+
readonly senderId?: Id;
|
|
338
|
+
/** `exact_search[receiver_id]`. */
|
|
339
|
+
readonly receiverId?: Id;
|
|
340
|
+
/**
|
|
341
|
+
* `exact_search[read_at]` set to the null sentinel, i.e. `read_at IS NULL`.
|
|
342
|
+
*
|
|
343
|
+
* Reading unread messages is what MARKS them read, so this filter is
|
|
344
|
+
* self-consuming: the same call that returns them empties the set it
|
|
345
|
+
* matched. Use {@link DirectMessagesNamespace.conversedUsers} for a badge
|
|
346
|
+
* count that survives being looked at.
|
|
347
|
+
*/
|
|
348
|
+
readonly unreadOnly?: boolean;
|
|
349
|
+
}
|
|
350
|
+
/** Body of {@link DirectMessagesNamespace.send}. */
|
|
351
|
+
export interface SendMessageInput {
|
|
352
|
+
/** Recipient. A user id string, never a handle. */
|
|
353
|
+
readonly receiverId: Id;
|
|
354
|
+
/**
|
|
355
|
+
* Up to {@link MESSAGE_CONTENT_MAX_LENGTH} characters.
|
|
356
|
+
*
|
|
357
|
+
* Required UNLESS `attachment` is present: `content` is `NOT NULL` with a
|
|
358
|
+
* `presence` validation, and the only thing that saves an attachment-only
|
|
359
|
+
* message is a `before_validation` hook that substitutes the literal string
|
|
360
|
+
* `"No Content"`. So an attachment sent with no caption comes back with
|
|
361
|
+
* `content: "No Content"`, in English, unlocalised, and every client has to
|
|
362
|
+
* special-case that string when rendering. Send a caption if you have one.
|
|
363
|
+
*/
|
|
364
|
+
readonly content?: string;
|
|
365
|
+
/**
|
|
366
|
+
* One file, up to {@link MESSAGE_ATTACHMENT_MAX_BYTES} - or
|
|
367
|
+
* {@link MESSAGE_IMAGE_MAX_BYTES} if the server decides it is an image.
|
|
368
|
+
*
|
|
369
|
+
* A React Native pick (`{ uri, name, type }`) may be passed straight through;
|
|
370
|
+
* the transport appends it verbatim to RN's `FormData` and refuses loudly on
|
|
371
|
+
* any other runtime, where it would silently upload as `"[object Object]"`.
|
|
372
|
+
*
|
|
373
|
+
* An `image/*` attachment is NOT stored as sent: `ImageProcessors::Message`
|
|
374
|
+
* re-encodes it to webp (quality 75, or 50 past 5 MiB) and renames it
|
|
375
|
+
* `message_image_<timestamp>.webp`, so `attachment_filename` on the record
|
|
376
|
+
* will not be the name you gave. Everything else is stored byte for byte.
|
|
377
|
+
*/
|
|
378
|
+
readonly attachment?: FileInput | NativeFile;
|
|
379
|
+
/**
|
|
380
|
+
* Id of a message to quote. Any message id the server can find - it is a
|
|
381
|
+
* plain `belongs_to :quoted_message, optional: true` with no validation that
|
|
382
|
+
* the quoted row is in this conversation, or even visible to either party.
|
|
383
|
+
* `quoted_preview` on the answer is rendered from it unconditionally, so
|
|
384
|
+
* quoting a stranger's message id leaks 120 characters of it. Only ever pass
|
|
385
|
+
* an id you took out of this same thread.
|
|
386
|
+
*/
|
|
387
|
+
readonly quotedMessageId?: MessageId;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* One-to-one messages, reachable as `oms.social.messages`.
|
|
391
|
+
*
|
|
392
|
+
* ## `list()` IS NOT A READ
|
|
393
|
+
*
|
|
394
|
+
* `MessagesController#index` runs an `update_all(read_at: Time.current)` before
|
|
395
|
+
* it serialises anything. Read {@link DirectMessagesNamespace.list} in full
|
|
396
|
+
* before you call it from a poller, a prefetch, or a retry loop.
|
|
397
|
+
*/
|
|
398
|
+
export declare class DirectMessagesNamespace extends Resource {
|
|
399
|
+
/**
|
|
400
|
+
* `GET /messages` - the caller's messages, sent and received, as one listing.
|
|
401
|
+
*
|
|
402
|
+
* ## This GET writes. Three ways it will surprise you.
|
|
403
|
+
*
|
|
404
|
+
* The controller marks the matched rows read before rendering, and it does
|
|
405
|
+
* so on the UNBOUNDED scope: `listing_scope.search(...).exact_search(...)
|
|
406
|
+
* .extra_options(...).update_all(read_at: Time.current)`. The page modifier
|
|
407
|
+
* is deliberately not in that chain, because marking only the first page
|
|
408
|
+
* would leave older messages showing unread. The consequences:
|
|
409
|
+
*
|
|
410
|
+
* 1. **An unfiltered `list()` marks your ENTIRE mailbox read**, every
|
|
411
|
+
* conversation, in one `UPDATE`. There is no read-only variant. Always
|
|
412
|
+
* pass {@link ListMessagesParams.withUser}.
|
|
413
|
+
* 2. **It also stamps `read_at` on messages YOU sent.** The listing scope is
|
|
414
|
+
* `sent_messages.or(received_messages)`, and `update_all` does not know
|
|
415
|
+
* the difference. So opening a thread sets `read_at` on your own outgoing
|
|
416
|
+
* messages, and any UI that renders "seen" from `read_at` on the sender's
|
|
417
|
+
* side is reading your own visit back to you as the recipient's. Treat
|
|
418
|
+
* `read_at` on a message whose `sender_id` is yours as meaningless.
|
|
419
|
+
* 3. **`updated_at` does NOT move**, because `update_all` skips callbacks.
|
|
420
|
+
* The index's `ETag` is computed from the relation's count and its maximum
|
|
421
|
+
* `updated_at`, so a conditional GET can answer `304` while `read_at`
|
|
422
|
+
* underneath has changed. The SDK never sends `If-None-Match`, so this
|
|
423
|
+
* only bites a host that added its own caching layer.
|
|
424
|
+
*
|
|
425
|
+
* Retrying is safe in the sense that the second `UPDATE` writes the same
|
|
426
|
+
* rows again; it is not safe in the sense that the damage is already done on
|
|
427
|
+
* attempt one.
|
|
428
|
+
*
|
|
429
|
+
* ## Ordering and paging
|
|
430
|
+
*
|
|
431
|
+
* The server declares no default order. Offset pagination over an unordered
|
|
432
|
+
* query in Postgres may repeat a row on one page and drop it from the next,
|
|
433
|
+
* so the SDK always sends one: `created_at:desc` unless you pass `order`.
|
|
434
|
+
* Newest first is what a chat view wants, since it opens at the bottom.
|
|
435
|
+
*
|
|
436
|
+
* With no page modifier at all the server would force `1:500`; the SDK sends
|
|
437
|
+
* an explicit page so {@link Paginated.pageSize} matches what the rows were
|
|
438
|
+
* counted against.
|
|
439
|
+
*
|
|
440
|
+
* ## Filters that do not exist
|
|
441
|
+
*
|
|
442
|
+
* `search_params` is `id, created_at, updated_at, content, sender_id,
|
|
443
|
+
* receiver_id, read_at`, and unknown keys FAIL CLOSED with `400 "Unknown
|
|
444
|
+
* search filter: ..."` rather than being dropped. So there is no filter for
|
|
445
|
+
* `attachment_kind`, none for `edited_at`, and no way to ask for "messages
|
|
446
|
+
* since X" other than `search[created_at]`, which is a partial string match
|
|
447
|
+
* on the rendered timestamp and is not a range query. Fetch and filter here.
|
|
448
|
+
*
|
|
449
|
+
* Costs one request against the general authenticated ceiling (600/min).
|
|
450
|
+
*/
|
|
451
|
+
list(params?: ListMessagesParams, options?: RequestOptions): Promise<Paginated<DirectMessage>>;
|
|
452
|
+
/**
|
|
453
|
+
* One conversation, newest first. `list()` with
|
|
454
|
+
* {@link ListMessagesParams.withUser} already filled in.
|
|
455
|
+
*
|
|
456
|
+
* This is the call a chat screen should make, and the reason it exists as its
|
|
457
|
+
* own method is the read-marking described on {@link list}: scoping the
|
|
458
|
+
* listing is what scopes the `UPDATE`. Opening a thread marks that thread
|
|
459
|
+
* read, which is what a user expects. Calling `list()` bare does not.
|
|
460
|
+
*/
|
|
461
|
+
conversation(userId: Id, params?: Omit<ListMessagesParams, "withUser">, options?: RequestOptions): Promise<Paginated<DirectMessage>>;
|
|
462
|
+
/**
|
|
463
|
+
* `GET /messages/conversed_users` - one row per person the caller has ever
|
|
464
|
+
* exchanged a message with, newest conversation first, each carrying the
|
|
465
|
+
* latest message either way and the caller's unread count from that sender.
|
|
466
|
+
*
|
|
467
|
+
* This is the inbox list, and it is the ONLY endpoint here that reports
|
|
468
|
+
* unread counts without destroying them: it does not mark anything read.
|
|
469
|
+
* Poll this one, not {@link list}.
|
|
470
|
+
*
|
|
471
|
+
* Not paginated and not filterable - it is a bespoke action, not a listing,
|
|
472
|
+
* so it carries no `ETag` and ignores every modifier. The whole set comes
|
|
473
|
+
* back on every call, and the cost is bounded by how many people you have
|
|
474
|
+
* talked to rather than by how many messages there are: one `DISTINCT ON`
|
|
475
|
+
* for the latest message per counterpart, one grouped `COUNT` for the unread
|
|
476
|
+
* numbers, and only that bounded set is hydrated.
|
|
477
|
+
*
|
|
478
|
+
* `last_message.preview` is 120 characters of content, or a bracketed
|
|
479
|
+
* placeholder (`"[image]"`, `"[audio]"`, `"[video]"`, `"[file]"`) when the
|
|
480
|
+
* newest message is an attachment. It is always a string, never `null`,
|
|
481
|
+
* despite what the web frontend's type says.
|
|
482
|
+
*
|
|
483
|
+
* A user you have messaged appears here even if one of you has since blocked
|
|
484
|
+
* the other; the rows are built from message history, not from
|
|
485
|
+
* {@link Relationship}. Cross-reference
|
|
486
|
+
* {@link RelationshipsNamespace.list} if the UI needs to hide those.
|
|
487
|
+
*/
|
|
488
|
+
conversedUsers(options?: RequestOptions): Promise<ConversedUser[]>;
|
|
489
|
+
/**
|
|
490
|
+
* `GET /messages/:id` - one message, by id.
|
|
491
|
+
*
|
|
492
|
+
* Scoped to `Current.user.messages`, so a message between two other people is
|
|
493
|
+
* `404 "Resource not found"` rather than `403`. Does NOT mark anything read,
|
|
494
|
+
* which makes it the safe way to re-read a single row.
|
|
495
|
+
*
|
|
496
|
+
* The `:extended` view is empty on `MessageBlueprint`, so this returns
|
|
497
|
+
* exactly the same fields as a row from {@link list}.
|
|
498
|
+
*/
|
|
499
|
+
get(id: MessageId, options?: RequestOptions): Promise<DirectMessage>;
|
|
500
|
+
/**
|
|
501
|
+
* `POST /messages` - sends one message. `201` with the created record.
|
|
502
|
+
*
|
|
503
|
+
* The sender is always the authenticated user; there is no way to say
|
|
504
|
+
* otherwise and `sender_id` in the body is ignored.
|
|
505
|
+
*
|
|
506
|
+
* ## What "the recipient blocked me" looks like
|
|
507
|
+
*
|
|
508
|
+
* `Message#creatable_by?` is `receiver.blockeds.exclude?(user)`, so a `401
|
|
509
|
+
* "You are not authorized to create this resource"` is how a block is
|
|
510
|
+
* announced - after the request, never before it, because the block that
|
|
511
|
+
* stops you is invisible to you: `Relationship`'s default scope hides rows
|
|
512
|
+
* where you are the `accepter` of a `block`. You cannot look up whether you
|
|
513
|
+
* are blocked; you can only be refused.
|
|
514
|
+
*
|
|
515
|
+
* The test is one-directional. If YOU blocked THEM, nothing here stops you
|
|
516
|
+
* from messaging them, and the message is delivered.
|
|
517
|
+
*
|
|
518
|
+
* ## Transport
|
|
519
|
+
*
|
|
520
|
+
* Sent as JSON when there is no attachment and as `multipart/form-data` when
|
|
521
|
+
* there is - the same route accepts both, and multipart is what makes a file
|
|
522
|
+
* possible at all. In multipart the numeric `quoted_message_id` goes out as
|
|
523
|
+
* text and Rails casts it back; that is normal and not a precision risk at
|
|
524
|
+
* these magnitudes.
|
|
525
|
+
*
|
|
526
|
+
* The field is named `attachment`. The controller still accepts a legacy
|
|
527
|
+
* `image` field and funnels it to the same blob, but nothing new should send
|
|
528
|
+
* it.
|
|
529
|
+
*
|
|
530
|
+
* ## What is refused here rather than by the server
|
|
531
|
+
*
|
|
532
|
+
* An image between {@link MESSAGE_IMAGE_MAX_BYTES} and
|
|
533
|
+
* {@link MESSAGE_ATTACHMENT_MAX_BYTES} passes the controller's cap and then
|
|
534
|
+
* dies inside the webp recompressor with a bare `ArgumentError`, which is a
|
|
535
|
+
* **500** with an error alert behind it rather than a `400`. This method
|
|
536
|
+
* throws before sending when the size is knowable. It stays silent when it is
|
|
537
|
+
* not - a `ReadableStream` with no declared `size`, or a picker that reported
|
|
538
|
+
* none - because buffering a file just to measure it would be worse than the
|
|
539
|
+
* server's answer.
|
|
540
|
+
*
|
|
541
|
+
* NOT retried: this is a `POST`, and a replay after a lost response sends the
|
|
542
|
+
* message twice.
|
|
543
|
+
*
|
|
544
|
+
* @throws {OmsError} `invalid_request` when neither content nor an
|
|
545
|
+
* attachment is present, when content is over the length cap, or when a
|
|
546
|
+
* knowably-oversized image would 500.
|
|
547
|
+
* @throws {OmsApiError} `401` when the recipient has blocked the caller,
|
|
548
|
+
* `400` when the recipient does not exist or the content is empty.
|
|
549
|
+
*/
|
|
550
|
+
send(input: SendMessageInput, options?: RequestOptions): Promise<DirectMessage>;
|
|
551
|
+
/**
|
|
552
|
+
* `PATCH /messages/:id` - rewrites the content of a message you sent.
|
|
553
|
+
*
|
|
554
|
+
* `content` is the only writable field. An attachment cannot be added,
|
|
555
|
+
* replaced or removed after the fact, and `receiver_id` is fixed.
|
|
556
|
+
*
|
|
557
|
+
* Allowed only while `Message#updatable_by?` holds: you are the sender AND
|
|
558
|
+
* the message is younger than {@link MESSAGE_EDIT_WINDOW_MS}. Past the
|
|
559
|
+
* window the answer is `401`, not `403` and not `422`. Use
|
|
560
|
+
* {@link canEditMessage} to grey the button out, and still handle the `401`:
|
|
561
|
+
* the window is measured on the server's clock.
|
|
562
|
+
*
|
|
563
|
+
* A successful edit stamps `edited_at`, which every client renders as an
|
|
564
|
+
* "edited" marker. Setting the same content back still stamps it - the
|
|
565
|
+
* controller's `before_update` only checks `content_changed?`, so a no-op
|
|
566
|
+
* PATCH with an identical string does NOT mark it edited, but any different
|
|
567
|
+
* string does, including whitespace.
|
|
568
|
+
*
|
|
569
|
+
* There is no cable broadcast for an edit. The other side sees the new text
|
|
570
|
+
* whenever it next fetches, and never learns that it changed.
|
|
571
|
+
*/
|
|
572
|
+
edit(id: MessageId, content: string, options?: RequestOptions): Promise<DirectMessage>;
|
|
573
|
+
/**
|
|
574
|
+
* `DELETE /messages/:id` - `204`, empty body.
|
|
575
|
+
*
|
|
576
|
+
* The sender may delete at any time; there is no edit window on destroy and
|
|
577
|
+
* the receiver cannot delete a message they were sent. The row and its blob
|
|
578
|
+
* go for both parties at once, so this is "unsend", not "hide from my copy".
|
|
579
|
+
*
|
|
580
|
+
* A message that another message quotes can still be deleted. The quoting
|
|
581
|
+
* row keeps its `quoted_preview` - the snapshot was serialised at render
|
|
582
|
+
* time from the association, so once the quoted row is gone the preview
|
|
583
|
+
* becomes `null` on the next fetch and the quote silently empties.
|
|
584
|
+
*
|
|
585
|
+
* Not retried, like every write. A `DELETE` replayed after a torn connection
|
|
586
|
+
* would answer `404` for a row it deleted perfectly well.
|
|
587
|
+
*/
|
|
588
|
+
delete(id: MessageId, options?: RequestOptions): Promise<void>;
|
|
589
|
+
/**
|
|
590
|
+
* `GET /messages/:id/attachment` - the bytes of the attached file.
|
|
591
|
+
*
|
|
592
|
+
* The route answers `302` to a presigned object-store URL, and `fetch`
|
|
593
|
+
* follows that redirect on its own. Which means this method works everywhere
|
|
594
|
+
* EXCEPT a browser in cookie mode: the request carries credentials, the
|
|
595
|
+
* redirected request arrives at the store with `Origin: null`, and the store
|
|
596
|
+
* answers a wildcard `Access-Control-Allow-Origin`, which CORS forbids for a
|
|
597
|
+
* credentialed request. There, use {@link attachmentUrl} and let an `<img>`,
|
|
598
|
+
* a `<video>` or an `<a download>` follow the redirect with no CORS check.
|
|
599
|
+
*
|
|
600
|
+
* Filename, byte size and kind are already on the {@link DirectMessage},
|
|
601
|
+
* which is why this hands back bare bytes.
|
|
602
|
+
*
|
|
603
|
+
* @throws {OmsError} `unsupported` when the redirect was blocked by CORS.
|
|
604
|
+
* @throws {OmsApiError} `404 "Message not found"` when the message is not
|
|
605
|
+
* yours, `404 "No attachment"` when it carries none.
|
|
606
|
+
*/
|
|
607
|
+
attachment(id: MessageId, options?: RequestOptions): Promise<Blob>;
|
|
608
|
+
/**
|
|
609
|
+
* Absolute URL for a message attachment, for an `<img>`, an `<a download>` or
|
|
610
|
+
* a new tab. In a browser in cookie mode this is the only way to reach the
|
|
611
|
+
* bytes; see {@link attachment}.
|
|
612
|
+
*
|
|
613
|
+
* Asynchronous because the credential has to be resolved first, and resolving
|
|
614
|
+
* it may involve a token refresh.
|
|
615
|
+
*
|
|
616
|
+
* - **cookie mode**: no credential in the URL; the browser attaches the
|
|
617
|
+
* host-only `oms_session` cookie itself. Do not put `crossorigin` on the
|
|
618
|
+
* element - that turns a no-cors load into a CORS one and re-creates the
|
|
619
|
+
* failure this method exists to avoid.
|
|
620
|
+
* - **token mode**: the token is appended as `?token=`, which is what a
|
|
621
|
+
* native client needs because an `<img>` cannot carry a header.
|
|
622
|
+
*
|
|
623
|
+
* THE TOKEN-MODE URL CONTAINS A LIVE CREDENTIAL. Build it at the moment of
|
|
624
|
+
* use, never store it, never log it, and never hand it to anything outside
|
|
625
|
+
* your own page - fetch the bytes with {@link attachment} and pass a `blob:`
|
|
626
|
+
* URL instead.
|
|
627
|
+
*
|
|
628
|
+
* There is a legacy twin at `/messages/:id/image` that the web frontend still
|
|
629
|
+
* links to from cached bundles and push deep-links. It resolves to the same
|
|
630
|
+
* blob for image messages and is not worth calling from new code.
|
|
631
|
+
*/
|
|
632
|
+
attachmentUrl(id: MessageId): Promise<string>;
|
|
633
|
+
}
|
|
634
|
+
/** Filter columns of `GET /relationships`, on top of {@link BASE_FILTER_COLUMNS}. */
|
|
635
|
+
export declare const RELATIONSHIP_FILTER_COLUMNS: readonly ["requester_id", "accepter_id"];
|
|
636
|
+
/** `extra_options` keys of `GET /relationships`. */
|
|
637
|
+
export declare const RELATIONSHIP_EXTRA_OPTION_KEYS: readonly ["user_id"];
|
|
638
|
+
/** `extra_options` of `GET /relationships`. `user_id` matches rows where the user is either end. */
|
|
639
|
+
export interface RelationshipExtraOptions {
|
|
640
|
+
readonly user_id?: Id;
|
|
641
|
+
}
|
|
642
|
+
/** Filters accepted by {@link RelationshipsNamespace.list}. */
|
|
643
|
+
export interface ListRelationshipsParams extends ListParams<(typeof RELATIONSHIP_FILTER_COLUMNS)[number], RelationshipExtraOptions> {
|
|
644
|
+
/**
|
|
645
|
+
* `extra_options[user_id]`: rows where this user is either end.
|
|
646
|
+
*
|
|
647
|
+
* Worth passing your own id even though it narrows nothing. The filter is
|
|
648
|
+
* what makes `QueryExtraOptions::Relationships` run at all, and running it is
|
|
649
|
+
* what applies its `INCLUDES = [:requester, :accepter]`. Without it the
|
|
650
|
+
* blueprint renders a full nested {@link User} for both ends of every row out
|
|
651
|
+
* of an unprepared query - two extra `SELECT`s per relationship.
|
|
652
|
+
*/
|
|
653
|
+
readonly userId?: Id;
|
|
654
|
+
/** `exact_search[requester_id]`: rows this user created. */
|
|
655
|
+
readonly requesterId?: Id;
|
|
656
|
+
/** `exact_search[accepter_id]`: rows aimed at this user. */
|
|
657
|
+
readonly accepterId?: Id;
|
|
658
|
+
}
|
|
659
|
+
/** Body of {@link RelationshipsNamespace.create}. */
|
|
660
|
+
export interface CreateRelationshipInput {
|
|
661
|
+
/** The other user. */
|
|
662
|
+
readonly userId: Id;
|
|
663
|
+
/** `"friend"` for a request, `"block"` for a block. */
|
|
664
|
+
readonly kind: RelationshipKind;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Friendships and blocks, reachable as `oms.social.relationships`.
|
|
668
|
+
*
|
|
669
|
+
* ## The state machine is TWO columns, not one
|
|
670
|
+
*
|
|
671
|
+
* There is no single `status` enum with a `blocked` member, however the UIs
|
|
672
|
+
* present it. A row carries `kind` (`friend` | `block`) and `status`
|
|
673
|
+
* (`pending` | `accepted`), and only three of the four combinations exist:
|
|
674
|
+
*
|
|
675
|
+
* | kind | status | what it is |
|
|
676
|
+
* | -------- | ---------- | ---------------------------------------------- |
|
|
677
|
+
* | `friend` | `pending` | a friend request, awaiting the accepter |
|
|
678
|
+
* | `friend` | `accepted` | a friendship |
|
|
679
|
+
* | `block` | `accepted` | the requester has blocked the accepter |
|
|
680
|
+
*
|
|
681
|
+
* `block` + `pending` is unreachable: a `before_create` forces a block to
|
|
682
|
+
* `accepted` at birth, which also means a block's status can never be edited
|
|
683
|
+
* afterwards (see the transition rules below). Blocking is asymmetric and
|
|
684
|
+
* direction matters - `requester_id` blocked `accepter_id`, never the reverse.
|
|
685
|
+
*
|
|
686
|
+
* ## Transitions the server will accept
|
|
687
|
+
*
|
|
688
|
+
* `PATCH` may write `status` and nothing else. `Relationship#before_update`
|
|
689
|
+
* aborts any change whose PREVIOUS value was `accepted`, so:
|
|
690
|
+
*
|
|
691
|
+
* - `pending -> accepted` is the one real transition, and it is what
|
|
692
|
+
* {@link accept} does;
|
|
693
|
+
* - `pending -> pending` is a no-op that answers `200`, because nothing
|
|
694
|
+
* changed and the guard only fires on a change;
|
|
695
|
+
* - `accepted -> pending` is refused, `400 "Status cannot be changed"`;
|
|
696
|
+
* - `accepted -> accepted` is a no-op `200`, same reason as above;
|
|
697
|
+
* - anything outside `pending` / `accepted` is refused by the inclusion
|
|
698
|
+
* validation, `400 "Status is not included in the list"`.
|
|
699
|
+
*
|
|
700
|
+
* There is no un-accept, no un-block and no "decline" verb. Ending ANY
|
|
701
|
+
* relationship - rejecting a request, unfriending, unblocking - is
|
|
702
|
+
* {@link delete} on the row. That is one method for three intentions, and the
|
|
703
|
+
* server cannot tell them apart either.
|
|
704
|
+
*
|
|
705
|
+
* ## EITHER PARTY CAN ACCEPT. INCLUDING THE ONE WHO ASKED.
|
|
706
|
+
*
|
|
707
|
+
* `Relationship#updatable_by?` is `accepter == user || requester == user`, and
|
|
708
|
+
* nothing narrows it for the `pending -> accepted` transition. So the sender of
|
|
709
|
+
* a friend request can `PATCH` their own request to `accepted` and become the
|
|
710
|
+
* recipient's friend without the recipient doing anything.
|
|
711
|
+
*
|
|
712
|
+
* That is a real hole, not a subtlety of this SDK, and it is load-bearing
|
|
713
|
+
* because friendship gates real things elsewhere in the API - a `friends`
|
|
714
|
+
* playlist becomes visible, the friends listening feed starts carrying that
|
|
715
|
+
* user's playback. Do not build a UI that offers the requester an accept
|
|
716
|
+
* button, and do not treat `status: "accepted"` as proof of consent from the
|
|
717
|
+
* accepter.
|
|
718
|
+
*
|
|
719
|
+
* ## A block you receive is INVISIBLE to you
|
|
720
|
+
*
|
|
721
|
+
* `Relationship` has `default_scope { where.not(accepter: Current.user, kind:
|
|
722
|
+
* "block") }`, which is applied to listings AND to lookups by id. You can see
|
|
723
|
+
* blocks you made; you cannot see, list, count or delete a block someone made
|
|
724
|
+
* against you. Its only observable effect is that
|
|
725
|
+
* {@link DirectMessagesNamespace.send} answers `401`.
|
|
726
|
+
*
|
|
727
|
+
* ## Realtime
|
|
728
|
+
*
|
|
729
|
+
* Both interesting events reach the other party as `NotificationsChannel`
|
|
730
|
+
* pushes rather than as anything on this namespace: `friendship_request`
|
|
731
|
+
* (`{ asker_id, asker_handle }`) when a `friend`/`pending` row is created, and
|
|
732
|
+
* `friendship_accepted` (`{ accepter_id, accepter_handle }`) when it flips.
|
|
733
|
+
* Neither carries the relationship row, so a client still has to
|
|
734
|
+
* {@link list} after the nudge. `block`, `delete` and every failed transition
|
|
735
|
+
* are silent.
|
|
736
|
+
*/
|
|
737
|
+
export declare class RelationshipsNamespace extends Resource {
|
|
738
|
+
/**
|
|
739
|
+
* `GET /relationships` - every relationship the caller is part of, in either
|
|
740
|
+
* direction, minus the blocks aimed at them.
|
|
741
|
+
*
|
|
742
|
+
* ## `kind` AND `status` ARE NOT FILTERABLE
|
|
743
|
+
*
|
|
744
|
+
* `search_params` on this controller is `requester_id, accepter_id` plus the
|
|
745
|
+
* inherited `id, created_at, updated_at`. `kind` and `status` are absent, and
|
|
746
|
+
* an unknown filter key does not get dropped - it is `400 "Unknown search
|
|
747
|
+
* filter: kind"`. So "list my friends" is not a query the API can answer:
|
|
748
|
+
* fetch the rows and filter here. {@link friends}, {@link incomingRequests},
|
|
749
|
+
* {@link outgoingRequests} and {@link blocked} do exactly that.
|
|
750
|
+
*
|
|
751
|
+
* This is the single most surprising thing about the endpoint, and it is why
|
|
752
|
+
* both the web frontend and the mobile app pull the whole set and filter in
|
|
753
|
+
* JavaScript rather than because nobody thought of it.
|
|
754
|
+
*
|
|
755
|
+
* ## Paging
|
|
756
|
+
*
|
|
757
|
+
* The SDK sends an explicit `modifiers[page]` and an explicit
|
|
758
|
+
* `created_at:desc` order, for the usual reason: with no page the server
|
|
759
|
+
* silently forces `1:500`, and offset paging over an unordered query can
|
|
760
|
+
* repeat and skip rows. `listRelationships()` in the mobile app sends
|
|
761
|
+
* neither, so it truncates at 500 rows without saying so - a real ceiling for
|
|
762
|
+
* an account with a long history, since blocks and dead requests count
|
|
763
|
+
* towards it.
|
|
764
|
+
*
|
|
765
|
+
* Indexes carry an `ETag`, so a conditional GET can come back `304`; the SDK
|
|
766
|
+
* does not send `If-None-Match` of its own.
|
|
767
|
+
*/
|
|
768
|
+
list(params?: ListRelationshipsParams, options?: RequestOptions): Promise<Paginated<Relationship>>;
|
|
769
|
+
/**
|
|
770
|
+
* Every relationship row, paged out in full and filtered in memory.
|
|
771
|
+
*
|
|
772
|
+
* The building block under {@link friends} and its siblings, exported
|
|
773
|
+
* because "fetch them all, then filter" is the only shape this endpoint
|
|
774
|
+
* supports and every client ends up writing it.
|
|
775
|
+
*
|
|
776
|
+
* `limit` caps how many rows are pulled, so a pathological account cannot
|
|
777
|
+
* turn one call into an unbounded walk. It defaults to 2000, which is four
|
|
778
|
+
* requests at the server's maximum page size.
|
|
779
|
+
*/
|
|
780
|
+
all(params?: ListRelationshipsParams, limit?: number, options?: RequestOptions): Promise<Relationship[]>;
|
|
781
|
+
/**
|
|
782
|
+
* The caller's accepted friends, as {@link User} records rather than as
|
|
783
|
+
* relationship rows.
|
|
784
|
+
*
|
|
785
|
+
* `selfId` is required and is NOT fetched for you: knowing which end of the
|
|
786
|
+
* row is "the other person" needs the caller's own id, and this namespace
|
|
787
|
+
* refuses to spend a `GET /sessions/mine` on every call to find it. Take it
|
|
788
|
+
* from `oms.account.me()` once and keep it.
|
|
789
|
+
*
|
|
790
|
+
* A friend appears exactly once even though the row could be in either
|
|
791
|
+
* direction. The result is derived from the same nested `requester` /
|
|
792
|
+
* `accepter` objects the listing already carries, so this costs no extra
|
|
793
|
+
* request beyond the paging {@link all} does.
|
|
794
|
+
*/
|
|
795
|
+
friends(selfId: Id, options?: RequestOptions): Promise<User[]>;
|
|
796
|
+
/**
|
|
797
|
+
* Friend requests waiting for the caller to answer: `friend` + `pending`
|
|
798
|
+
* rows where the caller is the ACCEPTER.
|
|
799
|
+
*
|
|
800
|
+
* These are the ones {@link accept} is meant for. Answer with {@link accept}
|
|
801
|
+
* or refuse with {@link delete}; there is no third verb.
|
|
802
|
+
*
|
|
803
|
+
* This walks the listing itself, as do {@link friends}, {@link blocked} and
|
|
804
|
+
* {@link outgoingRequests}, because the server cannot filter on `kind` or
|
|
805
|
+
* `status`. A screen that wants more than one of them should call
|
|
806
|
+
* {@link all} ONCE and sort the rows with {@link isFriendship},
|
|
807
|
+
* {@link isPendingRequest} and {@link isBlock} rather than paying for the
|
|
808
|
+
* same walk four times.
|
|
809
|
+
*/
|
|
810
|
+
incomingRequests(selfId: Id, options?: RequestOptions): Promise<Relationship[]>;
|
|
811
|
+
/**
|
|
812
|
+
* Friend requests the caller sent and nobody has answered: `friend` +
|
|
813
|
+
* `pending` rows where the caller is the REQUESTER.
|
|
814
|
+
*
|
|
815
|
+
* {@link delete} on one of these is a cancel. {@link accept} on one of these
|
|
816
|
+
* works too, and should not - see the class docs.
|
|
817
|
+
*/
|
|
818
|
+
outgoingRequests(selfId: Id, options?: RequestOptions): Promise<Relationship[]>;
|
|
819
|
+
/**
|
|
820
|
+
* Users the caller has blocked.
|
|
821
|
+
*
|
|
822
|
+
* Only blocks the caller MADE. A block made against the caller is hidden by
|
|
823
|
+
* the model's default scope and cannot be enumerated by any means; see the
|
|
824
|
+
* class docs.
|
|
825
|
+
*/
|
|
826
|
+
blocked(selfId: Id, options?: RequestOptions): Promise<User[]>;
|
|
827
|
+
/**
|
|
828
|
+
* `POST /relationships` - creates a row. `201` with the record.
|
|
829
|
+
*
|
|
830
|
+
* `requester_id` is forced to the authenticated user and `status` is not
|
|
831
|
+
* writable: a `friend` starts `pending` from the column default, a `block` is
|
|
832
|
+
* flipped to `accepted` by a `before_create`. Prefer {@link request} and
|
|
833
|
+
* {@link block}, which say which of the two you meant.
|
|
834
|
+
*
|
|
835
|
+
* @throws {OmsApiError} `400 "Cannot create relationship with yourself"`,
|
|
836
|
+
* or `400 "A relationship of this kind already exists between these
|
|
837
|
+
* users"` when a `friend` row already exists in either direction.
|
|
838
|
+
*/
|
|
839
|
+
create(input: CreateRelationshipInput, options?: RequestOptions): Promise<Relationship>;
|
|
840
|
+
/**
|
|
841
|
+
* Sends a friend request: `create({ kind: "friend" })`.
|
|
842
|
+
*
|
|
843
|
+
* At most one `friend` row may exist between two users in either direction,
|
|
844
|
+
* and the uniqueness check runs UNSCOPED - it sees rows the default scope
|
|
845
|
+
* hides from you. So a second request answers `400 "A relationship of this
|
|
846
|
+
* kind already exists between these users"`, and so does requesting someone
|
|
847
|
+
* who has already requested you (accept theirs instead) and, less obviously,
|
|
848
|
+
* requesting someone who has blocked you: their block row counts as an
|
|
849
|
+
* existing relationship. That last case is the only signal the API gives that
|
|
850
|
+
* a block against you exists, and it is indistinguishable from a duplicate
|
|
851
|
+
* request.
|
|
852
|
+
*
|
|
853
|
+
* The accepter gets a `friendship_request` notification over the cable.
|
|
854
|
+
*/
|
|
855
|
+
request(userId: Id, options?: RequestOptions): Promise<Relationship>;
|
|
856
|
+
/**
|
|
857
|
+
* Blocks a user: `create({ kind: "block" })`.
|
|
858
|
+
*
|
|
859
|
+
* Three things happen server-side that no other call does:
|
|
860
|
+
*
|
|
861
|
+
* 1. the row is born `accepted`, so its status is immediately frozen;
|
|
862
|
+
* 2. `destroy_existing_for_block` DELETES every existing relationship
|
|
863
|
+
* between the two of you first - an accepted friendship is gone, not
|
|
864
|
+
* suspended, and unblocking later does not bring it back;
|
|
865
|
+
* 3. that deletion is skipped when a block already exists between you in
|
|
866
|
+
* EITHER direction, which also means **blocking is not idempotent**: a
|
|
867
|
+
* second call creates a SECOND block row rather than answering with the
|
|
868
|
+
* first, because the uniqueness validation deliberately exempts blocks.
|
|
869
|
+
* Check {@link blocked} before calling, and unblock all the rows you find.
|
|
870
|
+
*
|
|
871
|
+
* The effect on the other user is exactly one thing: their
|
|
872
|
+
* {@link DirectMessagesNamespace.send} to you starts answering `401`. It does
|
|
873
|
+
* not stop you messaging them, it does not hide either profile, and it does
|
|
874
|
+
* not remove either of you from the other's
|
|
875
|
+
* {@link DirectMessagesNamespace.conversedUsers}, which is built from message
|
|
876
|
+
* history rather than from relationships.
|
|
877
|
+
*
|
|
878
|
+
* The blocked user is not notified.
|
|
879
|
+
*/
|
|
880
|
+
block(userId: Id, options?: RequestOptions): Promise<Relationship>;
|
|
881
|
+
/**
|
|
882
|
+
* `PATCH /relationships/:id` with `status: "accepted"` - accepts a pending
|
|
883
|
+
* friend request. `200` with the updated record.
|
|
884
|
+
*
|
|
885
|
+
* Only ever call this on a row from {@link incomingRequests}. The server will
|
|
886
|
+
* happily accept one of your OWN outgoing requests, which is a hole, not a
|
|
887
|
+
* feature; see the class docs.
|
|
888
|
+
*
|
|
889
|
+
* The requester gets a `friendship_accepted` notification.
|
|
890
|
+
*
|
|
891
|
+
* @throws {OmsApiError} `400 "Status cannot be changed"` when the row was
|
|
892
|
+
* already `accepted` and something is trying to move it back; `401` when
|
|
893
|
+
* the caller is neither end of the row; `404 "Resource not found"` when the
|
|
894
|
+
* id is not visible to the caller - which includes a block made against
|
|
895
|
+
* them.
|
|
896
|
+
*/
|
|
897
|
+
accept(id: RelationshipId, options?: RequestOptions): Promise<Relationship>;
|
|
898
|
+
/**
|
|
899
|
+
* `DELETE /relationships/:id` - `204`, empty body.
|
|
900
|
+
*
|
|
901
|
+
* The one verb for every ending: rejecting an incoming request, cancelling an
|
|
902
|
+
* outgoing one, unfriending, and unblocking. Either party may call it, and
|
|
903
|
+
* the row is gone for both - there is no soft state and no history.
|
|
904
|
+
*
|
|
905
|
+
* Unblocking does NOT restore the friendship the block destroyed on its way
|
|
906
|
+
* in. That row was deleted, not archived.
|
|
907
|
+
*
|
|
908
|
+
* Nobody is notified, so the other side finds out by noticing the row is no
|
|
909
|
+
* longer in their listing.
|
|
910
|
+
*
|
|
911
|
+
* `GET /relationships/:id` does not exist - the route is
|
|
912
|
+
* `only: [:create, :index, :destroy, :update]`, with no `show`. The web
|
|
913
|
+
* frontend's `Relationship.get` calls it anyway and can only ever have got a
|
|
914
|
+
* routing error back. Read a single row out of {@link list} instead.
|
|
915
|
+
*/
|
|
916
|
+
delete(id: RelationshipId, options?: RequestOptions): Promise<void>;
|
|
917
|
+
}
|
|
918
|
+
/** Body of {@link GroupChatsNamespace.create}. */
|
|
919
|
+
export interface CreateGroupChatInput {
|
|
920
|
+
/**
|
|
921
|
+
* Required, up to {@link GROUP_CHAT_NAME_MAX_LENGTH}. Blank, whitespace-only
|
|
922
|
+
* and the null sentinel are all rejected with `400 "Name is required"`.
|
|
923
|
+
*/
|
|
924
|
+
readonly name: string;
|
|
925
|
+
/**
|
|
926
|
+
* Everyone else to put in the chat, by user id.
|
|
927
|
+
*
|
|
928
|
+
* De-duplicated, stripped of blanks, and the caller's own id is removed - the
|
|
929
|
+
* creator is added separately, as the chat's `admin`. Ids that do not name a
|
|
930
|
+
* real user are DROPPED IN SILENCE: `User.where(id: member_ids)` simply
|
|
931
|
+
* matches fewer rows, the call still answers `201`, and the only way to know
|
|
932
|
+
* a member is missing is to count `members` on the answer.
|
|
933
|
+
*
|
|
934
|
+
* There is no friendship check and no invitation step. Anyone can be put into
|
|
935
|
+
* a chat by anyone, and finds out by seeing it in their listing.
|
|
936
|
+
*/
|
|
937
|
+
readonly memberIds?: readonly Id[];
|
|
938
|
+
}
|
|
939
|
+
/** Body of {@link GroupChatMessagesNamespace.send}. */
|
|
940
|
+
export interface SendGroupChatMessageInput {
|
|
941
|
+
/**
|
|
942
|
+
* Up to {@link GROUP_CHAT_MESSAGE_CONTENT_MAX_LENGTH} - four thousand
|
|
943
|
+
* characters here, against one thousand for a direct message.
|
|
944
|
+
*
|
|
945
|
+
* Optional when `attachment` is present, and genuinely optional: unlike
|
|
946
|
+
* {@link SendMessageInput.content}, nothing substitutes a placeholder, so an
|
|
947
|
+
* attachment-only message comes back with `content: null`.
|
|
948
|
+
*/
|
|
949
|
+
readonly content?: string;
|
|
950
|
+
/**
|
|
951
|
+
* One file, up to {@link MESSAGE_ATTACHMENT_MAX_BYTES}.
|
|
952
|
+
*
|
|
953
|
+
* Stored byte for byte. There is no webp recompression here and no
|
|
954
|
+
* pixel-bomb check either, which is the opposite of what
|
|
955
|
+
* {@link DirectMessagesNamespace.send} does with an image, so the 20 MiB
|
|
956
|
+
* image ceiling does NOT apply and `attachment_content_type` on the answer is
|
|
957
|
+
* whatever you uploaded.
|
|
958
|
+
*/
|
|
959
|
+
readonly attachment?: FileInput | NativeFile;
|
|
960
|
+
}
|
|
961
|
+
/** Cursor for {@link GroupChatMessagesNamespace.list}. */
|
|
962
|
+
export interface ListGroupChatMessagesParams {
|
|
963
|
+
/**
|
|
964
|
+
* Return messages created strictly AFTER this one. Sent as `after_id`.
|
|
965
|
+
*
|
|
966
|
+
* Read {@link GroupChatMessagesNamespace.list} before using it: this cursor
|
|
967
|
+
* fails open, and the failure looks like a working request.
|
|
968
|
+
*/
|
|
969
|
+
readonly afterId?: GroupChatMessageId;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Many-to-many chats, reachable as `oms.social.groupChats`.
|
|
973
|
+
*
|
|
974
|
+
* ## THERE IS NO REALTIME HERE AT ALL
|
|
975
|
+
*
|
|
976
|
+
* Direct messages at least push a `message_received` notification over the
|
|
977
|
+
* cable. Group chats push NOTHING: no channel streams them, no `Notification`
|
|
978
|
+
* row is written when a message lands, and the only thing a new group message
|
|
979
|
+
* triggers server-side is a Discord alert for the operators. A group chat UI
|
|
980
|
+
* has to poll {@link GroupChatMessagesNamespace.list} with an `after_id`
|
|
981
|
+
* cursor, and there is no endpoint that will tell it which chats changed
|
|
982
|
+
* without listing them.
|
|
983
|
+
*
|
|
984
|
+
* The cheapest honest loop is {@link GroupChatsNamespace.list} on a slow timer
|
|
985
|
+
* for the chat list (it carries `last_message` per chat, so one request covers
|
|
986
|
+
* every conversation) and
|
|
987
|
+
* {@link GroupChatMessagesNamespace.list} with `afterId` on a fast timer for
|
|
988
|
+
* the chat that is open. Both are on the general 600/min ceiling shared with
|
|
989
|
+
* the rest of the app.
|
|
990
|
+
*
|
|
991
|
+
* ## Every id in this family is a STRING
|
|
992
|
+
*
|
|
993
|
+
* Unlike {@link DirectMessage} and {@link Relationship} two sections up.
|
|
994
|
+
*
|
|
995
|
+
* ## Only one `kind` exists
|
|
996
|
+
*
|
|
997
|
+
* `GroupChat::KINDS` is `["ad_hoc"]` and the controller hardcodes it on
|
|
998
|
+
* create, so `kind` is always `"ad_hoc"`. `system_managed`, `context_type` and
|
|
999
|
+
* `context_id` are there for a future subsystem that hangs its own chats off
|
|
1000
|
+
* the model; nothing writes them today. The web frontend types the kind as
|
|
1001
|
+
* `"site"`, which is a value the backend has never emitted.
|
|
1002
|
+
*
|
|
1003
|
+
* ## Errors here have a different SHAPE from the rest of the API
|
|
1004
|
+
*
|
|
1005
|
+
* `GroupChatsController` raises `not_found!` and `unauthorized!` with no
|
|
1006
|
+
* argument, so those answers carry the JSON body `null` rather than the bare
|
|
1007
|
+
* error string every CRUD controller sends. Branch on the status, not on the
|
|
1008
|
+
* body.
|
|
1009
|
+
*/
|
|
1010
|
+
export declare class GroupChatsNamespace extends Resource {
|
|
1011
|
+
/** Messages inside a chat. */
|
|
1012
|
+
readonly messages: GroupChatMessagesNamespace;
|
|
1013
|
+
constructor(http: ConstructorParameters<typeof Resource>[0]);
|
|
1014
|
+
/**
|
|
1015
|
+
* `GET /group_chats` - every chat the caller is a member of, most recently
|
|
1016
|
+
* touched first.
|
|
1017
|
+
*
|
|
1018
|
+
* Returns a bare array and not a {@link Paginated}, because the endpoint
|
|
1019
|
+
* genuinely has no paging: it is a hand-written action, not a CRUD index, so
|
|
1020
|
+
* `modifiers[page]`, `search[...]` and the `ETag` are all absent and the
|
|
1021
|
+
* whole list comes back on every call. That is fine while a user has tens of
|
|
1022
|
+
* chats and is a cliff if one ever has thousands.
|
|
1023
|
+
*
|
|
1024
|
+
* `updated_at` is the sort key and it is touched by a new message
|
|
1025
|
+
* (`chat.touch` in the message controller), by a rename, and by nothing else
|
|
1026
|
+
* - a deleted message does not move a chat back down the list.
|
|
1027
|
+
*
|
|
1028
|
+
* Each chat carries a `last_message` summary, computed for the whole page in
|
|
1029
|
+
* one window-function query. That summary is what makes this endpoint usable
|
|
1030
|
+
* as a poll for "did anything change anywhere": compare `last_message.id`
|
|
1031
|
+
* per chat instead of listing every conversation.
|
|
1032
|
+
*
|
|
1033
|
+
* Members are NOT included - this is the default blueprint view. Use
|
|
1034
|
+
* {@link get} for the roster.
|
|
1035
|
+
*
|
|
1036
|
+
* A site administrator sees every chat on the instance here, not just their
|
|
1037
|
+
* own; `GroupChat.viewable_by` returns `all` for an admin.
|
|
1038
|
+
*/
|
|
1039
|
+
list(options?: RequestOptions): Promise<GroupChat[]>;
|
|
1040
|
+
/**
|
|
1041
|
+
* `GET /group_chats/:id` - one chat with its member roster.
|
|
1042
|
+
*
|
|
1043
|
+
* The `:extended` view, which is the default view PLUS `members`; every
|
|
1044
|
+
* blueprint view here inherits the base fields rather than narrowing them.
|
|
1045
|
+
* Each member row carries `user_handle` and `user_name` denormalised, so
|
|
1046
|
+
* rendering a roster needs no further requests.
|
|
1047
|
+
*
|
|
1048
|
+
* Not a member? `404 "Resource not found"` - the id is not distinguishable
|
|
1049
|
+
* from one that does not exist, which is the right answer.
|
|
1050
|
+
*
|
|
1051
|
+
* Note that this route reaches the generic CRUD `show`, so it is one of the
|
|
1052
|
+
* few places in this file whose 404 body is the usual string rather than
|
|
1053
|
+
* `null`.
|
|
1054
|
+
*/
|
|
1055
|
+
get(id: GroupChatId, options?: RequestOptions): Promise<GroupChatDetail>;
|
|
1056
|
+
/**
|
|
1057
|
+
* `POST /group_chats` - creates a chat and its roster in one transaction.
|
|
1058
|
+
* `201` with the `:extended` view.
|
|
1059
|
+
*
|
|
1060
|
+
* The caller becomes the chat's `admin`; everyone in
|
|
1061
|
+
* {@link CreateGroupChatInput.memberIds} becomes a `member`. There is no way
|
|
1062
|
+
* to create a chat you are not in, and no way to hand out a second admin
|
|
1063
|
+
* afterwards - `role` is not writable through any route in this namespace, so
|
|
1064
|
+
* the creator is the only administrator the chat will ever have. If they
|
|
1065
|
+
* leave, the chat keeps working for everybody but can never be renamed and
|
|
1066
|
+
* can never take another member.
|
|
1067
|
+
*
|
|
1068
|
+
* Not retried: a replay after a lost response creates a second chat.
|
|
1069
|
+
*
|
|
1070
|
+
* @throws {OmsError} `invalid_request` when the name is blank here.
|
|
1071
|
+
* @throws {OmsApiError} `400 "Name is required"` when the server judges it
|
|
1072
|
+
* blank.
|
|
1073
|
+
*/
|
|
1074
|
+
create(input: CreateGroupChatInput, options?: RequestOptions): Promise<GroupChatDetail>;
|
|
1075
|
+
/**
|
|
1076
|
+
* `PATCH /group_chats/:id` - renames the chat. `200` with the `:extended`
|
|
1077
|
+
* view.
|
|
1078
|
+
*
|
|
1079
|
+
* The name is the ONLY writable field: `kind`, `system_managed` and the
|
|
1080
|
+
* context pair are ignored if sent.
|
|
1081
|
+
*
|
|
1082
|
+
* Passing `null` clears the name, and so does passing `""` or any
|
|
1083
|
+
* whitespace-only string - the controller's `clean_string` collapses all
|
|
1084
|
+
* three to `nil` and the column is nullable, so there is no way to set a
|
|
1085
|
+
* blank name and no error telling you it was dropped. A nameless chat is
|
|
1086
|
+
* legal and every client has to fall back to listing the members.
|
|
1087
|
+
*
|
|
1088
|
+
* Restricted to the chat's admin (its creator) or a site administrator.
|
|
1089
|
+
* Anyone else gets `401` with a `null` body.
|
|
1090
|
+
*/
|
|
1091
|
+
rename(id: GroupChatId, name: string | null, options?: RequestOptions): Promise<GroupChatDetail>;
|
|
1092
|
+
/**
|
|
1093
|
+
* `POST /group_chats/:id/members` - adds one user. `200` (NOT `201`) with the
|
|
1094
|
+
* `:extended` view.
|
|
1095
|
+
*
|
|
1096
|
+
* Idempotent by design: a user who is already a member is not re-added and
|
|
1097
|
+
* the current chat comes back unchanged, so this is safe to retry and safe to
|
|
1098
|
+
* call from a UI that is not sure of its own state.
|
|
1099
|
+
*
|
|
1100
|
+
* Admin-only, and the new member joins as a plain `member` - there is no way
|
|
1101
|
+
* to promote anyone.
|
|
1102
|
+
*
|
|
1103
|
+
* A `user_id` that names nobody is `400 "user_id is required"`, which is the
|
|
1104
|
+
* same message an absent `user_id` gets and is therefore not a useful
|
|
1105
|
+
* diagnostic.
|
|
1106
|
+
*/
|
|
1107
|
+
addMember(id: GroupChatId, userId: Id, options?: RequestOptions): Promise<GroupChatDetail>;
|
|
1108
|
+
/**
|
|
1109
|
+
* `DELETE /group_chats/:id/members/:user_id` - `204`, empty body.
|
|
1110
|
+
*
|
|
1111
|
+
* Two callers are allowed and they mean different things: the chat's admin
|
|
1112
|
+
* removing somebody, and any member removing THEMSELVES, which is how you
|
|
1113
|
+
* leave. {@link leave} is the same call under a name that says so.
|
|
1114
|
+
*
|
|
1115
|
+
* ## Removing the last member DESTROYS THE CHAT
|
|
1116
|
+
*
|
|
1117
|
+
* `chat.destroy! if chat.members.empty?` runs immediately after the removal,
|
|
1118
|
+
* and `GroupChat has_many :messages, dependent: :destroy`. So the last person
|
|
1119
|
+
* to leave takes the entire history with them, for everyone, with no
|
|
1120
|
+
* confirmation and no way back. There is no `DELETE /group_chats/:id` -
|
|
1121
|
+
* emptying the roster is the only way a chat is ever deleted, and it is easy
|
|
1122
|
+
* to trigger by accident on a two-person chat.
|
|
1123
|
+
*
|
|
1124
|
+
* ## An admin can strand a chat
|
|
1125
|
+
*
|
|
1126
|
+
* The admin leaving is permitted and promotes nobody. The chat survives with
|
|
1127
|
+
* members and no administrator: nobody can rename it and nobody can add
|
|
1128
|
+
* anyone ever again. Warn before letting the creator leave a chat that still
|
|
1129
|
+
* has people in it.
|
|
1130
|
+
*
|
|
1131
|
+
* Removing someone who is not a member is a `204` with no effect, not a 404.
|
|
1132
|
+
*/
|
|
1133
|
+
removeMember(id: GroupChatId, userId: Id, options?: RequestOptions): Promise<void>;
|
|
1134
|
+
/**
|
|
1135
|
+
* Leaves a chat: {@link removeMember} with the caller's own id.
|
|
1136
|
+
*
|
|
1137
|
+
* `selfId` is passed rather than looked up, for the same reason
|
|
1138
|
+
* {@link RelationshipsNamespace.friends} takes one - the SDK will not spend a
|
|
1139
|
+
* session round trip on every call to learn who it is.
|
|
1140
|
+
*
|
|
1141
|
+
* If you are the last member this DESTROYS the chat and every message in it.
|
|
1142
|
+
* If you are the admin it leaves the chat permanently unadministered. Both
|
|
1143
|
+
* are described on {@link removeMember} and both are worth a confirmation
|
|
1144
|
+
* dialog.
|
|
1145
|
+
*/
|
|
1146
|
+
leave(id: GroupChatId, selfId: Id, options?: RequestOptions): Promise<void>;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Messages inside a group chat, reachable as
|
|
1150
|
+
* `oms.social.groupChats.messages`.
|
|
1151
|
+
*
|
|
1152
|
+
* Every route is nested under the chat, and every one of them resolves the
|
|
1153
|
+
* chat first: a caller who is not a member gets `404` on the CHAT before the
|
|
1154
|
+
* message id is even looked at, so there is no way to probe for message ids.
|
|
1155
|
+
*/
|
|
1156
|
+
export declare class GroupChatMessagesNamespace extends Resource {
|
|
1157
|
+
/**
|
|
1158
|
+
* `GET /group_chats/:id/messages` - up to
|
|
1159
|
+
* {@link GROUP_CHAT_MESSAGE_PAGE_SIZE} messages, OLDEST FIRST.
|
|
1160
|
+
*
|
|
1161
|
+
* This endpoint does not speak the list DSL. No `search`, no `exact_search`,
|
|
1162
|
+
* no `modifiers`, no `ETag`; the page size is a constant in the controller
|
|
1163
|
+
* and `modifiers[page]` is ignored rather than rejected. The only knob is
|
|
1164
|
+
* `after_id`, and it has two sharp edges.
|
|
1165
|
+
*
|
|
1166
|
+
* ## `afterId` FAILS OPEN
|
|
1167
|
+
*
|
|
1168
|
+
* The controller looks the anchor up with `chat.messages.find_by(id: after)`
|
|
1169
|
+
* and then applies the filter only `if anchor`. An id it cannot find - a
|
|
1170
|
+
* message that was deleted, an id from a different chat, a typo - does not
|
|
1171
|
+
* produce an error. The cursor is silently DROPPED and you get the first 100
|
|
1172
|
+
* messages of the chat from the very beginning.
|
|
1173
|
+
*
|
|
1174
|
+
* A client that pages by remembering the last id it saw, and whose last id
|
|
1175
|
+
* gets deleted, therefore restarts at the top and appends the same 100
|
|
1176
|
+
* messages forever. Guard it: if the first row you get back is one you have
|
|
1177
|
+
* already seen, your cursor is gone, not your data.
|
|
1178
|
+
*
|
|
1179
|
+
* ## `afterId` SKIPS TIES
|
|
1180
|
+
*
|
|
1181
|
+
* The filter is `created_at > anchor.created_at`, on the timestamp alone
|
|
1182
|
+
* rather than on `(created_at, id)`. Two messages written in the same
|
|
1183
|
+
* microsecond - which is how a burst of system messages arrives - and
|
|
1184
|
+
* anchoring on the first one drops the second permanently from a forward
|
|
1185
|
+
* walk.
|
|
1186
|
+
*
|
|
1187
|
+
* ## THERE IS NO BACKWARD CURSOR
|
|
1188
|
+
*
|
|
1189
|
+
* `before_id` does not exist. The only entry point into a chat's history is
|
|
1190
|
+
* its beginning, and the only direction is forward. Opening a 5000-message
|
|
1191
|
+
* chat at the newest message costs 50 sequential requests, and there is no
|
|
1192
|
+
* way to fetch the tail directly. For the common "what is new" case, keep the
|
|
1193
|
+
* newest id you have seen and poll with it; for a first open, either walk
|
|
1194
|
+
* with {@link all} or accept that you are showing the top of the chat.
|
|
1195
|
+
*
|
|
1196
|
+
* System messages (`system_kind` set, `sender_id` null) are in the same
|
|
1197
|
+
* stream and are meant to render as centred pills rather than as messages.
|
|
1198
|
+
* {@link isSystemMessage} tests for them.
|
|
1199
|
+
*/
|
|
1200
|
+
list(chatId: GroupChatId, params?: ListGroupChatMessagesParams, options?: RequestOptions): Promise<GroupChatMessage[]>;
|
|
1201
|
+
/**
|
|
1202
|
+
* Walks a chat's history forward from the beginning and returns it in one
|
|
1203
|
+
* array, oldest first.
|
|
1204
|
+
*
|
|
1205
|
+
* One request per {@link GROUP_CHAT_MESSAGE_PAGE_SIZE} messages, which is the
|
|
1206
|
+
* only shape the endpoint allows. `limit` caps the walk at 1000 messages (ten
|
|
1207
|
+
* requests) by default so a long chat cannot quietly spend a large share of
|
|
1208
|
+
* the 600/min ceiling; raise it deliberately.
|
|
1209
|
+
*
|
|
1210
|
+
* Stops early on a short page, and also stops if the server hands back a page
|
|
1211
|
+
* whose first row it has already seen - that is the fail-open cursor
|
|
1212
|
+
* described on {@link list}, and continuing would loop forever.
|
|
1213
|
+
*/
|
|
1214
|
+
all(chatId: GroupChatId, limit?: number, options?: RequestOptions): Promise<GroupChatMessage[]>;
|
|
1215
|
+
/**
|
|
1216
|
+
* `POST /group_chats/:id/messages` - sends one message. `201` with the
|
|
1217
|
+
* record.
|
|
1218
|
+
*
|
|
1219
|
+
* Content, an attachment, or both; a message with neither is `400 "Message
|
|
1220
|
+
* must have content or an attachment"`. The sender is always the
|
|
1221
|
+
* authenticated user, and non-members never get this far - the chat lookup
|
|
1222
|
+
* answers `404` first.
|
|
1223
|
+
*
|
|
1224
|
+
* Sending TOUCHES the chat, which is what moves it to the top of
|
|
1225
|
+
* {@link GroupChatsNamespace.list}. Nothing else in this namespace does, so a
|
|
1226
|
+
* chat whose only recent activity was a rename or a deletion sorts by its own
|
|
1227
|
+
* `updated_at` rather than by its newest message.
|
|
1228
|
+
*
|
|
1229
|
+
* Sent as JSON with no attachment and as `multipart/form-data` with one.
|
|
1230
|
+
*
|
|
1231
|
+
* Nobody is notified. See the {@link GroupChatsNamespace} docs: there is no
|
|
1232
|
+
* cable channel, no notification row, and no push for group chat messages, so
|
|
1233
|
+
* the only way anyone learns about this is by polling.
|
|
1234
|
+
*
|
|
1235
|
+
* Not retried: a replay after a lost response posts the message twice.
|
|
1236
|
+
*
|
|
1237
|
+
* @throws {OmsError} `invalid_request` when neither content nor attachment is
|
|
1238
|
+
* given, or content is over the cap.
|
|
1239
|
+
*/
|
|
1240
|
+
send(chatId: GroupChatId, input: SendGroupChatMessageInput, options?: RequestOptions): Promise<GroupChatMessage>;
|
|
1241
|
+
/**
|
|
1242
|
+
* `PATCH /group_chats/:id/messages/:id` - rewrites the content. `200`.
|
|
1243
|
+
*
|
|
1244
|
+
* Content only, and it must be non-blank: an empty edit is `400 "Content
|
|
1245
|
+
* required"` rather than a way to clear the text. An attachment cannot be
|
|
1246
|
+
* changed or removed.
|
|
1247
|
+
*
|
|
1248
|
+
* Allowed for the sender within {@link MESSAGE_EDIT_WINDOW_MS} of
|
|
1249
|
+
* `created_at`, and for a site administrator with no window at all. A system
|
|
1250
|
+
* message is never editable by anybody. Past the window: `401`.
|
|
1251
|
+
*
|
|
1252
|
+
* `edited_at` is stamped only when the content actually differs, so
|
|
1253
|
+
* re-sending the same string is a genuine no-op rather than a way to mark a
|
|
1254
|
+
* message edited.
|
|
1255
|
+
*
|
|
1256
|
+
* Does NOT touch the chat, so an edit does not reorder
|
|
1257
|
+
* {@link GroupChatsNamespace.list}.
|
|
1258
|
+
*/
|
|
1259
|
+
edit(chatId: GroupChatId, messageId: GroupChatMessageId, content: string, options?: RequestOptions): Promise<GroupChatMessage>;
|
|
1260
|
+
/**
|
|
1261
|
+
* `DELETE /group_chats/:id/messages/:id` - `204`, empty body.
|
|
1262
|
+
*
|
|
1263
|
+
* The sender may delete at ANY time - unlike editing, destroying has no
|
|
1264
|
+
* fifteen-minute window - and so may a site administrator. A system message
|
|
1265
|
+
* cannot be deleted by anyone. The row and its blob go for everybody.
|
|
1266
|
+
*
|
|
1267
|
+
* Deleting does not touch the chat, so {@link GroupChatsNamespace.list} keeps
|
|
1268
|
+
* its old ordering, and it invalidates any `afterId` cursor pointing at the
|
|
1269
|
+
* deleted message: the next {@link list} call with that cursor silently
|
|
1270
|
+
* restarts at the top of the chat. See {@link list}.
|
|
1271
|
+
*/
|
|
1272
|
+
delete(chatId: GroupChatId, messageId: GroupChatMessageId, options?: RequestOptions): Promise<void>;
|
|
1273
|
+
/**
|
|
1274
|
+
* `GET /group_chats/:id/messages/:id/attachment` - the bytes of the attached
|
|
1275
|
+
* file.
|
|
1276
|
+
*
|
|
1277
|
+
* A `302` to a presigned object-store URL, which `fetch` follows on its own
|
|
1278
|
+
* everywhere except a browser in cookie mode; see
|
|
1279
|
+
* {@link DirectMessagesNamespace.attachment} for why, and use
|
|
1280
|
+
* {@link attachmentUrl} there.
|
|
1281
|
+
*
|
|
1282
|
+
* Filename, byte size and content type are on the {@link GroupChatMessage}
|
|
1283
|
+
* itself - but only when it HAS an attachment: the blueprint declares those
|
|
1284
|
+
* three fields with an `if:` guard, so they are ABSENT rather than `null` on
|
|
1285
|
+
* a message without one. Test `has_attachment`, not
|
|
1286
|
+
* `attachment_filename !== null`.
|
|
1287
|
+
*
|
|
1288
|
+
* @throws {OmsError} `unsupported` when the redirect was blocked by CORS.
|
|
1289
|
+
* @throws {OmsApiError} `404` with a `null` body when the chat is not the
|
|
1290
|
+
* caller's or the message is not in it; `404 "No attachment"` when it
|
|
1291
|
+
* carries none.
|
|
1292
|
+
*/
|
|
1293
|
+
attachment(chatId: GroupChatId, messageId: GroupChatMessageId, options?: RequestOptions): Promise<Blob>;
|
|
1294
|
+
/**
|
|
1295
|
+
* Absolute URL for a group chat attachment, for an `<img>`, an
|
|
1296
|
+
* `<a download>` or a new tab.
|
|
1297
|
+
*
|
|
1298
|
+
* Same rules as {@link DirectMessagesNamespace.attachmentUrl}: no credential
|
|
1299
|
+
* in cookie mode, `?token=` in token mode, and THE TOKEN-MODE URL IS A LIVE
|
|
1300
|
+
* CREDENTIAL - build it at the moment of use and never store, log or share
|
|
1301
|
+
* it.
|
|
1302
|
+
*
|
|
1303
|
+
* The web frontend's `groupChatAttachmentUrl` builds the same path with no
|
|
1304
|
+
* credential at all, which works in the browser (the cookie rides along) and
|
|
1305
|
+
* silently 401s anywhere else.
|
|
1306
|
+
*/
|
|
1307
|
+
attachmentUrl(chatId: GroupChatId, messageId: GroupChatMessageId): Promise<string>;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The user at the other end of a relationship row, given the caller's own id.
|
|
1311
|
+
*
|
|
1312
|
+
* A relationship is stored with a direction (`requester` asked, `accepter` was
|
|
1313
|
+
* asked) that a friends list does not care about, so every client writes this
|
|
1314
|
+
* function. It is here once instead.
|
|
1315
|
+
*
|
|
1316
|
+
* Returns `undefined` when `selfId` is neither end of the row, and when the
|
|
1317
|
+
* nested user object is missing - which the API does not currently do, but the
|
|
1318
|
+
* mobile app's own type marks both associations optional, so it is treated as
|
|
1319
|
+
* possible rather than asserted away.
|
|
1320
|
+
*/
|
|
1321
|
+
export declare function counterpart(relationship: Relationship, selfId: Id): User | undefined;
|
|
1322
|
+
/** `kind === "friend" && status === "accepted"`. */
|
|
1323
|
+
export declare function isFriendship(relationship: Relationship): boolean;
|
|
1324
|
+
/** An unanswered friend request, in either direction. */
|
|
1325
|
+
export declare function isPendingRequest(relationship: Relationship): boolean;
|
|
1326
|
+
/**
|
|
1327
|
+
* A block the CALLER made. There is no such thing as a visible block against
|
|
1328
|
+
* the caller - see {@link RelationshipsNamespace}.
|
|
1329
|
+
*/
|
|
1330
|
+
export declare function isBlock(relationship: Relationship): boolean;
|
|
1331
|
+
/** A system message: rendered as a centred pill, never editable or deletable. */
|
|
1332
|
+
export declare function isSystemMessage(message: GroupChatMessage): boolean;
|
|
1333
|
+
/**
|
|
1334
|
+
* Whether a message is still inside its edit window, for greying out a button.
|
|
1335
|
+
*
|
|
1336
|
+
* Measured against the CALLER's clock, and the server measures against its own,
|
|
1337
|
+
* so this is an approximation that gets less honest the further the two drift.
|
|
1338
|
+
* Always handle the `401` as well; do not treat `true` here as a promise that
|
|
1339
|
+
* the edit will land, and do not treat `false` as a reason to skip the call if
|
|
1340
|
+
* the user insists.
|
|
1341
|
+
*
|
|
1342
|
+
* Takes `now` so the function stays pure and the module stays isolate-safe -
|
|
1343
|
+
* no `Date.now()` at module scope, and a caller can pass a server-derived
|
|
1344
|
+
* clock if it has one.
|
|
1345
|
+
*/
|
|
1346
|
+
export declare function canEditMessage(message: {
|
|
1347
|
+
created_at: Timestamp;
|
|
1348
|
+
}, now?: number): boolean;
|