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