@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.
Files changed (96) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +4800 -610
  3. package/dist/types/client.d.ts +60 -3
  4. package/dist/types/http.d.ts +444 -19
  5. package/dist/types/index.d.ts +5 -1
  6. package/dist/types/internal/attachments.d.ts +17 -0
  7. package/dist/types/internal/helpers.d.ts +9 -0
  8. package/dist/types/listing.d.ts +90 -0
  9. package/dist/types/resources/account.d.ts +71 -5
  10. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  11. package/dist/types/resources/admin/chests.d.ts +51 -0
  12. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  13. package/dist/types/resources/admin/identities.d.ts +99 -0
  14. package/dist/types/resources/admin/index.d.ts +77 -0
  15. package/dist/types/resources/admin/jobs.d.ts +97 -0
  16. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  17. package/dist/types/resources/admin/notepads.d.ts +49 -0
  18. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  19. package/dist/types/resources/admin/quotas.d.ts +114 -0
  20. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  21. package/dist/types/resources/admin/types.d.ts +222 -0
  22. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  23. package/dist/types/resources/admin.d.ts +1841 -0
  24. package/dist/types/resources/auth/index.d.ts +39 -0
  25. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  26. package/dist/types/resources/auth/sessions.d.ts +844 -0
  27. package/dist/types/resources/chests.d.ts +54 -3
  28. package/dist/types/resources/content/analysis.d.ts +62 -0
  29. package/dist/types/resources/content/blogs.d.ts +471 -0
  30. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  31. package/dist/types/resources/content/index.d.ts +52 -0
  32. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  33. package/dist/types/resources/content/intel/config.d.ts +135 -0
  34. package/dist/types/resources/content/intel/index.d.ts +53 -0
  35. package/dist/types/resources/content/intel/items.d.ts +91 -0
  36. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  37. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  38. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  39. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  40. package/dist/types/resources/content/intel/types.d.ts +86 -0
  41. package/dist/types/resources/content/jokes.d.ts +93 -0
  42. package/dist/types/resources/content/notifications.d.ts +147 -0
  43. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  44. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  45. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  46. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  47. package/dist/types/resources/content.d.ts +2775 -0
  48. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  49. package/dist/types/resources/forms.d.ts +176 -35
  50. package/dist/types/resources/index.d.ts +19 -8
  51. package/dist/types/resources/ipLookup.d.ts +20 -4
  52. package/dist/types/resources/jobs.d.ts +69 -25
  53. package/dist/types/resources/library/annotations.d.ts +162 -0
  54. package/dist/types/resources/library/books.d.ts +534 -0
  55. package/dist/types/resources/library/chat.d.ts +259 -0
  56. package/dist/types/resources/library/index.d.ts +39 -0
  57. package/dist/types/resources/library/shelves.d.ts +237 -0
  58. package/dist/types/resources/library/types.d.ts +120 -0
  59. package/dist/types/resources/library.d.ts +1433 -0
  60. package/dist/types/resources/linkTrees.d.ts +142 -30
  61. package/dist/types/resources/media.d.ts +351 -0
  62. package/dist/types/resources/movies/addons.d.ts +388 -0
  63. package/dist/types/resources/movies/collections.d.ts +352 -0
  64. package/dist/types/resources/movies/index.d.ts +29 -0
  65. package/dist/types/resources/movies/types.d.ts +50 -0
  66. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  67. package/dist/types/resources/movies.d.ts +1160 -0
  68. package/dist/types/resources/music/artists.d.ts +1068 -0
  69. package/dist/types/resources/music/imports.d.ts +941 -0
  70. package/dist/types/resources/music/index.d.ts +61 -0
  71. package/dist/types/resources/music/playlists.d.ts +1027 -0
  72. package/dist/types/resources/music/social.d.ts +1132 -0
  73. package/dist/types/resources/music/songs.d.ts +1167 -0
  74. package/dist/types/resources/notepads.d.ts +4 -1
  75. package/dist/types/resources/quotas.d.ts +7 -1
  76. package/dist/types/resources/realtime.d.ts +855 -0
  77. package/dist/types/resources/shortLinks.d.ts +49 -5
  78. package/dist/types/resources/social/groupChats.d.ts +458 -0
  79. package/dist/types/resources/social/index.d.ts +19 -0
  80. package/dist/types/resources/social/messages.d.ts +424 -0
  81. package/dist/types/resources/social/relationships.d.ts +322 -0
  82. package/dist/types/resources/social/types.d.ts +37 -0
  83. package/dist/types/resources/social.d.ts +1348 -0
  84. package/dist/types/resources/storage/upload.d.ts +158 -11
  85. package/dist/types/resources/storage.d.ts +111 -27
  86. package/dist/types/resources/tickets.d.ts +101 -13
  87. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  88. package/dist/types/resources/tools/captions.d.ts +448 -21
  89. package/dist/types/resources/tools/downloader.d.ts +21 -0
  90. package/dist/types/resources/tools/index.d.ts +57 -15
  91. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  92. package/dist/types/resources/tools/transcription.d.ts +35 -13
  93. package/dist/types/resources/tools/upscale.d.ts +23 -3
  94. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  95. package/dist/types/types.d.ts +249 -17
  96. 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;