Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
623 linesCodeBlameRaw
1/**
2 * Chat: channels, direct messages, threads and messages, kept by the chat
3 * service (`services/chat`). Members are people; messages from the agents
4 * workspaces once had stay readable under the names they had.
5 *
6 * Wire shapes are snake_case end to end, so the site, the public API and
7 * the live socket all carry the same objects.
8 */
9import type { ServiceBinding } from "./clients";
10import type { User } from "./identity";
11import type { AskPage } from "./integrations";
12import type { Result } from "./result";
13
14/**
15 * Who is speaking: a person (by user id), g1t itself (`system:g1t`, the one
16 * reserved actor all of g1t's own work shows as), or, on older messages, a
17 * former workspace agent (by agent id).
18 */
19export type Principal = { kind: "user" | "agent" | "system"; id: string };
20
21/**
22 * g1t in chat: @g1t, the same reserved `system` actor (id `g1t`) the
23 * runner and the feed show g1t's own work as. Every member can talk with
24 * it in a direct message, where it answers with the workspace's own AI
25 * provider (docs.g1t.sh/guides/ask/). It is never a person: the People
26 * directory and member pickers never list it.
27 */
28export const G1T_PRINCIPAL: Principal = { kind: "system", id: "g1t" };
29/** `system:g1t`, as chat keeps members and authors. */
30export const G1T_KEY = "system:g1t";
31
32/** Whether a member is g1t itself. */
33export function isG1t(member: { kind: string; id?: string } | null | undefined): boolean {
34 return member?.kind === "system" && member.id === "g1t";
35}
36
37export function principalKey(principal: Principal): string {
38 return `${principal.kind}:${principal.id}`;
39}
40
41export function parsePrincipalKey(key: string): Principal | null {
42 // g1t is the only `system` member there is.
43 if (key === G1T_KEY) return { ...G1T_PRINCIPAL };
44 const at = key.indexOf(":");
45 if (at < 0) return null;
46 const kind = key.slice(0, at);
47 const id = key.slice(at + 1);
48 if ((kind !== "user" && kind !== "agent") || !id) return null;
49 return { kind, id };
50}
51
52/** How a member shows: resolved by the chat service when it answers. */
53export type MemberProfile = Principal & {
54 /** `username` for a person (lowercased: what `@` mentions), `handle` for an agent. */
55 name: string;
56 /**
57 * A person's username as they wrote it (`Ana`), when that differs from
58 * `name`; absent for an agent. Shown in their card and in autocomplete.
59 */
60 display_username?: string | null;
61 /**
62 * What chat shows them as: a person's display name, else their username
63 * in its chosen case; an agent's display name. Read it with `memberName`.
64 */
65 display_name: string;
66 /** Uploaded avatar hash for a person, or null for the letter avatar. */
67 avatar: string | null;
68 /** An agent's one-line role ("Release manager for g1t"). */
69 role: string | null;
70 /**
71 * An agent's title ("QA Engineer"); null for a person. Chat always sets
72 * it; optional so profiles a page makes for itself need not.
73 */
74 title?: string | null;
75 /** What an agent's face is drawn from when it chose none; null for a person. Always set by chat. */
76 avatar_seed?: string | null;
77 /**
78 * An archived agent (docs.g1t.sh/guides/chat/, "Archived and deleted
79 * agents"): when, and by whom. Its conversations stay readable; it is
80 * never woken and never listed to talk to. Null or absent for a live
81 * agent and for a person.
82 */
83 archived_at?: string | null;
84 archived_by?: string | null;
85 /** An agent deleted for good: its messages stay under the name it had. Absent otherwise. */
86 deleted?: boolean;
87};
88
89/**
90 * A member's handle as it shows (`@Ana`, without the `@`): a person's
91 * username in its chosen case, an agent's handle.
92 */
93export function memberHandle(member: { name: string; display_username?: string | null }): string {
94 const display = member.display_username;
95 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
96}
97
98/**
99 * How a member's name is shown everywhere in chat (messages, the
100 * sidebar, direct-message titles, typing, notifications and pushes): their
101 * display name, else their handle in its chosen case. One rule, so every
102 * surface agrees.
103 */
104export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string {
105 return member.display_name?.trim() || memberHandle(member);
106}
107
108export type ChannelKind = "channel" | "dm";
109
110export type Channel = {
111 id: string;
112 workspace_id: string;
113 kind: ChannelKind;
114 /** Lowercase, no `#`. Null for a direct message. */
115 name: string | null;
116 topic: string | null;
117 private: boolean;
118 created_by: Principal;
119 created_at: string;
120 archived_at: string | null;
121 last_message_at: string | null;
122};
123
124export type ChannelMember = {
125 channel_id: string;
126 member: MemberProfile;
127 role: "owner" | "member";
128 starred: boolean;
129 muted: boolean;
130 last_read_id: string | null;
131 joined_at: string;
132};
133
134/** A card g1t or an agent posts: an event, a task, an approval. */
135export type MessageCard = {
136 /** What it is about, e.g. `pull`, `issue`, `task`, `deploy`, `approval`, `session`, `draft_issue`. */
137 kind: string;
138 title: string;
139 /** A short line under the title: "3/3 checks · +214 −87". */
140 detail: string | null;
141 /** A status shown on the right: "Needs approval", "Merged". */
142 state: string | null;
143 /** Where clicking the card goes, relative to the site. */
144 href: string | null;
145 /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
146 body?: string | null;
147 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
148 fields?: CardField[];
149 /**
150 * What people can do right here. Pressing one goes to the service that
151 * owns the card (`owner`), which checks the person may, does it, and
152 * updates the card in place. A card without `owner` has no actions.
153 */
154 actions?: CardAction[];
155 /** The service whose card this was and that answered its actions: `agents`, on older cards only. */
156 owner?: "agents" | null;
157 /** What the card is about, for its owner: a session id, a draft id. */
158 ref?: string | null;
159};
160
161export type CardField = { label: string; value: string };
162
163/** A button on a card. */
164export type CardAction = {
165 /** Unique on the card: `stop`, `approve`, `file`. */
166 id: string;
167 label: string;
168 style?: "primary" | "danger" | "default";
169 /** Asked before it runs: "Stop this session and everything under it?". */
170 confirm?: string | null;
171 /** A value it needs first, asked inline: an amount in dollars, or a line of text. */
172 input?: { kind: "money" | "text"; label: string; placeholder?: string | null; initial?: string | null } | null;
173 /** A link instead of an action: opens this place in the site. */
174 href?: string | null;
175};
176
177export type ChatMessage = {
178 /** Time-sortable (ULID-like), so ordering by id is ordering by time. */
179 id: string;
180 channel_id: string;
181 author: MemberProfile;
182 kind: "text" | "card";
183 body: string;
184 card: MessageCard | null;
185 /** The message this replies under, or null for a top-level message. */
186 thread_root: string | null;
187 reply_count: number;
188 last_reply_at: string | null;
189 created_at: string;
190 edited_at: string | null;
191 deleted_at: string | null;
192 /**
193 * Its reactions, one per emoji in the order each was first used. Chat
194 * always sets it; optional so messages a page makes for itself need not.
195 */
196 reactions?: ChatReaction[];
197 /**
198 * On a message from @g1t that answers someone: how the answer went, who
199 * wrote it, and the steps it took. Absent on every other message.
200 */
201 answer?: ChatAnswer | null;
202 /**
203 * On a question someone asked @g1t from a page (the top bar's Ask): the
204 * page they were on, which g1t is told. Absent otherwise.
205 */
206 page?: AskPage | null;
207 /**
208 * A "New conversation" mark in a direct message with @g1t: answers after
209 * it read only what was said after it. Absent otherwise.
210 */
211 reset?: boolean;
212};
213
214/**
215 * Where an answer from @g1t is: `thinking` until its first words,
216 * `streaming` while they come (live as `message.streaming`), then `done`,
217 * `stopped` (Stop, or a newer question took over) or `error`.
218 */
219export type ChatAnswerState = "thinking" | "streaming" | "done" | "stopped" | "error";
220
221export type ChatAnswer = {
222 state: ChatAnswerState;
223 /** The provider's name and the model, once the provider said. */
224 provider_label?: string | null;
225 model?: string | null;
226 /** `max_tokens`, `length`: cut off at its limit. */
227 stop_reason?: string | null;
228 /** What went wrong, for `error`. */
229 error?: string | null;
230 /**
231 * Steps the answer took on its way (tool calls and the like), as the
232 * integrations service streamed them: each `ask` event other than meta,
233 * text, done and error, kept as it came. At most 50.
234 */
235 steps?: Record<string, unknown>[];
236 /** Set when Ask is not set up in the workspace: the answer says where to set it up. */
237 setup?: "missing" | null;
238};
239
240/**
241 * One emoji's reactions on a message. `emoji` is a Unicode emoji or
242 * `:name:` for one of the workspace's own (`CustomEmoji`).
243 */
244export type ChatReaction = {
245 emoji: string;
246 /** Everyone who reacted with it, people and agents alike. */
247 count: number;
248 /**
249 * Whether the viewer did. In live `message.*` events, which everyone in
250 * the room gets, it is always false: keep your own from what you know.
251 */
252 me: boolean;
253 /** The first ten who reacted with it, for the hover list. */
254 by: MemberProfile[];
255};
256
257/** Who may add a workspace's emoji: every member (the default), or only its owners. */
258export type EmojiUpload = "members" | "admins";
259
260/**
261 * One of a workspace's own emoji, used as `:name:`. Its image is served
262 * from the usercontent origin at `/emoji/<file>` (never from the site).
263 */
264export type CustomEmoji = {
265 name: string;
266 /** For an alias, the emoji it is another name for; it shows that one's image. */
267 alias_of: string | null;
268 /** The SHA-256 of the image's bytes. */
269 file: string;
270 content_type: "image/png" | "image/gif" | "image/webp";
271 bytes: number;
272 created_by: MemberProfile;
273 created_at: string;
274};
275
276export type EmojiList = {
277 emoji: CustomEmoji[];
278 emoji_upload: EmojiUpload;
279 /** Whether the viewer may add emoji and aliases. */
280 can_upload: boolean;
281 /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */
282 can_manage: boolean;
283};
284
285/** Who may do something in a workspace's chat: every member, or only its owners. */
286export type ChatAllowed = "members" | "owners";
287
288/**
289 * Who may rename and archive a channel: its owners (whoever made it) and
290 * the workspace's owners (the default), or the workspace's owners only.
291 */
292export type ChannelManagers = "channel_owners" | "owners";
293
294/**
295 * A workspace's chat settings, which its owners choose (Workspace,
296 * Settings, Chat). The chat service enforces each one; the page only
297 * hides what the viewer may not do.
298 */
299export type ChatSettings = {
300 /** Who may create public channels. */
301 public_channels: ChatAllowed;
302 /** Who may create private channels. */
303 private_channels: ChatAllowed;
304 /** Who may rename, archive and unarchive channels. */
305 manage_channels: ChannelManagers;
306 /** Who may add custom emoji (`admins` is the workspace's owners). */
307 emoji_upload: EmojiUpload;
308 /**
309 * The public channels someone new is put in the first time they open
310 * Chat, by id. #general unless the owners chose otherwise.
311 */
312 default_channels: string[];
313};
314
315/** What the viewer may do in a workspace's chat, from its settings and their role. */
316export type ChatPermissions = {
317 create_public_channels: boolean;
318 create_private_channels: boolean;
319 add_emoji: boolean;
320 /** Whether they may change the workspace's chat settings: owners only. */
321 manage_settings: boolean;
322};
323
324/** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */
325export type ChatSettingsView = {
326 settings: ChatSettings;
327 can: ChatPermissions;
328 channels: Channel[];
329};
330
331/** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */
332export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean };
333
334/** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */
335export type EmojiFile = { data: string };
336
337/** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */
338export const MAX_EMOJI_BYTES = 256 * 1024;
339export const MAX_EMOJI_SIDE = 512;
340
341/** One row of the Chat sidebar. */
342export type ChatSidebarEntry = {
343 channel: Channel;
344 /** "g1t-core", or the other members' names for a direct message. */
345 title: string;
346 /** For a direct message: who else is in it (up to four). */
347 others: MemberProfile[];
348 starred: boolean;
349 muted: boolean;
350 unread: number;
351 mentions: number;
352};
353
354export type ChatSidebar = {
355 entries: ChatSidebarEntry[];
356 /** Public channels in the workspace the viewer has not joined. */
357 browsable: number;
358 /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */
359 can: ChatPermissions;
360};
361
362/**
363 * `activity`: what was said over a span, in the conversations the viewer
364 * can read (every public channel, and the private channels and direct
365 * messages they are in). Messages are text messages and thread replies
366 * that were not deleted; cards agents post are not messages. Home reads it
367 * for what people and agents said since you were last there.
368 */
369export type ChatActivity = {
370 /** RFC 3339: `[from, until)`. */
371 from: string;
372 until: string;
373 messages: number;
374 /** Conversations with at least one message. */
375 channels: number;
376 /** Who said how much: member keys (`user:<id>`, `agent:<id>`), most first. */
377 authors: { key: string; messages: number }[];
378};
379
380export type MessagePage = {
381 messages: ChatMessage[];
382 /** Pass as `before` to read further back; null at the beginning. */
383 older: string | null;
384 /**
385 * For a thread's first page: the message the thread is under, however
386 * many replies it has, so a thread panel always shows it (a session's
387 * live card, say) at its top.
388 */
389 root?: ChatMessage | null;
390 /**
391 * Set when the page was read with `after`: pass it as `after` again for
392 * the next messages, or null when this page reached the newest.
393 */
394 newer?: string | null;
395};
396
397export type NewChannel = { name: string; topic?: string | null; private?: boolean };
398
399export type PostMessage = {
400 body: string;
401 thread_root?: string | null;
402 /** In a direct message with @g1t: the page the person asks from, which g1t is told. Ignored elsewhere. */
403 page?: AskPage | null;
404};
405
406/**
407 * What the live socket sends. The site opens
408 * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the
409 * session and forwards the upgrade to the chat service with the viewer.
410 */
411export type ChatLiveEvent =
412 | { type: "message.created"; message: ChatMessage }
413 | { type: "message.updated"; message: ChatMessage }
414 | { type: "message.deleted"; channel_id: string; id: string }
415 /**
416 * An answer from @g1t as it is written: the whole text so far (not a
417 * delta, so a late or dropped frame never garbles it) and where it is.
418 * The stored message catches up with a `message.updated` at the end.
419 */
420 | { type: "message.streaming"; channel_id: string; id: string; body: string; answer: ChatAnswer }
421 | { type: "typing"; channel_id: string; member: MemberProfile; until: string }
422 | { type: "read"; channel_id: string; principal: Principal; last_read_id: string }
423 | { type: "channel.updated"; channel: Channel }
424 | { type: "reaction.added" | "reaction.removed"; channel_id: string; message_id: string; emoji: string; member: MemberProfile };
425
426/**
427 * Header the site sets on a forwarded live socket: the viewer, as JSON.
428 * The site forwards the upgrade to the chat service's
429 * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out,
430 * at the cost of looking up each of the viewer's workspaces).
431 */
432export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer";
433
434/** A channel with its members, and whether the viewer may rename or archive it. */
435export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean };
436
437export type ChatApi = {
438 sidebar(workspace: string, viewer: User): Promise<Result<ChatSidebar>>;
439 /** What was said in `[from, until)` (RFC 3339) where the viewer can read, counted. */
440 activity(workspace: string, viewer: User, span: { from: string; until: string }): Promise<Result<ChatActivity>>;
441 channel(
442 workspace: string,
443 channelId: string,
444 viewer: User,
445 ): Promise<Result<ChannelDetail>>;
446 /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */
447 channelByName(
448 workspace: string,
449 name: string,
450 viewer: User,
451 ): Promise<Result<ChannelDetail>>;
452 /**
453 * Browse channels: every public channel, and the private ones the viewer
454 * is in. With `archived`, the archived ones instead.
455 */
456 browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>;
457 /** Makes a channel. Who may make a public or a private one is the workspace's setting. */
458 createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>;
459 /**
460 * Renames, archives or unarchives a channel (the workspace's
461 * `manage_channels` setting says who may), or changes its topic (any
462 * member). #general is never renamed or archived. Everyone looking at it
463 * gets `channel.updated`.
464 */
465 updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>;
466 /** The workspace's chat settings, for any member; only owners may change them. */
467 chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>;
468 /** Owners only: changes some of the workspace's chat settings, and answers all of them. */
469 setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>;
470 /**
471 * The direct message between the viewer and these members, created on
472 * first use. The same set of members always gets the same channel.
473 */
474 openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>;
475 join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
476 leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
477 /** Adds a person. An invite grants read, never write. */
478 invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>;
479 /**
480 * Newest first. Without `thread_root`, the channel's top-level messages;
481 * with it, that thread's replies, plus the message they reply to as the
482 * oldest once the page reaches the start of the thread (`older` null).
483 * With `after` (catching up after a reconnect): the messages after that
484 * id instead, oldest first, deleted ones included so the client can
485 * drop them; `newer` says whether there are more.
486 * Limit 50 by default, 200 at most.
487 */
488 messages(
489 workspace: string,
490 channelId: string,
491 viewer: User,
492 page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null },
493 ): Promise<Result<MessagePage>>;
494 post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>;
495 edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>;
496 remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
497 markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
498 setPreferences(
499 workspace: string,
500 channelId: string,
501 viewer: User,
502 prefs: { starred?: boolean; muted?: boolean },
503 ): Promise<Result<null>>;
504 /**
505 * Reacts to a message with `emoji` (one Unicode emoji, or `:name:` for
506 * one of the workspace's own). Once per member per emoji; at most 50
507 * different emoji on a message. Answers the message's reactions now.
508 */
509 react(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
510 unreact(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
511 /** The workspace's own emoji, by name, and who may add them. */
512 listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>;
513 /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */
514 addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>;
515 /** Gives an existing emoji (`target`) another name. */
516 aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>;
517 /** Removes an emoji, and its aliases with it. Its creator or an owner may. */
518 removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>;
519 /** Owners only: who may add emoji. */
520 setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>;
521 /** The viewer's direct message with @g1t, created on first use. */
522 g1tDm(workspace: string, viewer: User): Promise<Result<Channel>>;
523 /**
524 * Starts a new conversation in the viewer's direct message with @g1t: a
525 * mark after which answers read only what comes next. Stops an answer
526 * still coming.
527 */
528 g1tReset(workspace: string, channelId: string, viewer: User): Promise<Result<ChatMessage>>;
529 /** Has @g1t answer the latest question again, in place of its last answer. */
530 g1tRetry(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
531 /** Stops @g1t's answer still coming; what came stays. */
532 g1tStop(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
533};
534
535/**
536 * For g1t's own services only, never the site on a person's behalf: posts
537 * and edits messages as @g1t in a direct message with it. The chat service
538 * answers these at `POST /internal/<method>`, which no route from the site
539 * reaches, and refuses anything that came in from the internet.
540 */
541export type ChatInternalApi = {
542 /** Posts `body` as @g1t in a direct message with it, in the workspace with this slug. */
543 g1tPost(workspace: string, channelId: string, body: string): Promise<Result<ChatMessage>>;
544 /** Changes the text of one of @g1t's messages there. */
545 g1tEdit(workspace: string, channelId: string, id: string, body: string): Promise<Result<ChatMessage>>;
546};
547
548export function chatInternalClient(service: ServiceBinding): ChatInternalApi {
549 const call = async <T>(method: string, args: object): Promise<T> => {
550 const response = await service.fetch(`https://service/internal/${method}`, {
551 method: "POST",
552 headers: { "content-type": "application/json" },
553 body: JSON.stringify(args),
554 });
555 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
556 return (await response.json()) as T;
557 };
558 return {
559 g1tPost: (workspace, channelId, body) => call("g1t_post", { workspace, channel_id: channelId, body }),
560 g1tEdit: (workspace, channelId, id, body) => call("g1t_edit", { workspace, channel_id: channelId, id, body }),
561 };
562}
563
564async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
565 const response = await service.fetch(`https://service/rpc/${method}`, {
566 method: "POST",
567 headers: { "content-type": "application/json" },
568 body: JSON.stringify(args),
569 });
570 if (!response.ok) {
571 throw new Error(`${method} failed with status ${response.status}`);
572 }
573 return (await response.json()) as T;
574}
575
576export function chatClient(service: ServiceBinding): ChatApi {
577 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
578 return {
579 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
580 activity: (workspace, viewer, span) => call("activity", { workspace, viewer, from: span.from, until: span.until }),
581 channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }),
582 channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }),
583 browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }),
584 createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }),
585 updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }),
586 chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }),
587 setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }),
588 openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }),
589 join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }),
590 leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }),
591 invite: (workspace, channelId, viewer, member) =>
592 call("invite", { workspace, channel_id: channelId, viewer, member }),
593 messages: (workspace, channelId, viewer, page) =>
594 call("messages", {
595 workspace,
596 channel_id: channelId,
597 viewer,
598 before: page?.before ?? null,
599 after: page?.after ?? null,
600 limit: page?.limit ?? null,
601 thread_root: page?.thread_root ?? null,
602 }),
603 post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }),
604 edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }),
605 remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }),
606 markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }),
607 setPreferences: (workspace, channelId, viewer, prefs) =>
608 call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }),
609 react: (workspace, channelId, viewer, messageId, emoji) =>
610 call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
611 unreact: (workspace, channelId, viewer, messageId, emoji) =>
612 call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
613 listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }),
614 addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }),
615 aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }),
616 removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }),
617 setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }),
618 g1tDm: (workspace, viewer) => call("g1t_dm", { workspace, viewer }),
619 g1tReset: (workspace, channelId, viewer) => call("g1t_reset", { workspace, channel_id: channelId, viewer }),
620 g1tRetry: (workspace, channelId, viewer) => call("g1t_retry", { workspace, channel_id: channelId, viewer }),
621 g1tStop: (workspace, channelId, viewer) => call("g1t_stop", { workspace, channel_id: channelId, viewer }),
622 };
623}