@bruch/max-client 0.1.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 (174) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +168 -0
  3. package/UPSTREAM.md +10 -0
  4. package/dist/api/account.d.ts +24 -0
  5. package/dist/api/account.d.ts.map +1 -0
  6. package/dist/api/auth.d.ts +57 -0
  7. package/dist/api/auth.d.ts.map +1 -0
  8. package/dist/api/bots.d.ts +8 -0
  9. package/dist/api/bots.d.ts.map +1 -0
  10. package/dist/api/chats.d.ts +41 -0
  11. package/dist/api/chats.d.ts.map +1 -0
  12. package/dist/api/facade.d.ts +19 -0
  13. package/dist/api/facade.d.ts.map +1 -0
  14. package/dist/api/index.d.ts +10 -0
  15. package/dist/api/index.d.ts.map +1 -0
  16. package/dist/api/messages.d.ts +35 -0
  17. package/dist/api/messages.d.ts.map +1 -0
  18. package/dist/api/response.d.ts +7 -0
  19. package/dist/api/response.d.ts.map +1 -0
  20. package/dist/api/runtime.d.ts +19 -0
  21. package/dist/api/runtime.d.ts.map +1 -0
  22. package/dist/api/uploads.d.ts +23 -0
  23. package/dist/api/uploads.d.ts.map +1 -0
  24. package/dist/api/users.d.ts +25 -0
  25. package/dist/api/users.d.ts.map +1 -0
  26. package/dist/app.d.ts +55 -0
  27. package/dist/app.d.ts.map +1 -0
  28. package/dist/auth/flows.d.ts +27 -0
  29. package/dist/auth/flows.d.ts.map +1 -0
  30. package/dist/auth/index.d.ts +3 -0
  31. package/dist/auth/index.d.ts.map +1 -0
  32. package/dist/auth/index.js +109 -0
  33. package/dist/auth/index.js.map +11 -0
  34. package/dist/auth/providers.d.ts +11 -0
  35. package/dist/auth/providers.d.ts.map +1 -0
  36. package/dist/client.d.ts +139 -0
  37. package/dist/client.d.ts.map +1 -0
  38. package/dist/config.d.ts +80 -0
  39. package/dist/config.d.ts.map +1 -0
  40. package/dist/connection/connection.d.ts +25 -0
  41. package/dist/connection/connection.d.ts.map +1 -0
  42. package/dist/connection/index.d.ts +4 -0
  43. package/dist/connection/index.d.ts.map +1 -0
  44. package/dist/connection/pending.d.ts +9 -0
  45. package/dist/connection/pending.d.ts.map +1 -0
  46. package/dist/connection/readers.d.ts +17 -0
  47. package/dist/connection/readers.d.ts.map +1 -0
  48. package/dist/dispatch/dispatcher.d.ts +16 -0
  49. package/dist/dispatch/dispatcher.d.ts.map +1 -0
  50. package/dist/dispatch/index.d.ts +3 -0
  51. package/dist/dispatch/index.d.ts.map +1 -0
  52. package/dist/dispatch/router.d.ts +64 -0
  53. package/dist/dispatch/router.d.ts.map +1 -0
  54. package/dist/errors.d.ts +24 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/files/file.d.ts +29 -0
  57. package/dist/files/file.d.ts.map +1 -0
  58. package/dist/files/index.d.ts +2 -0
  59. package/dist/files/index.d.ts.map +1 -0
  60. package/dist/files/index.js +95 -0
  61. package/dist/files/index.js.map +10 -0
  62. package/dist/formatting/index.d.ts +2 -0
  63. package/dist/formatting/index.d.ts.map +1 -0
  64. package/dist/formatting/markdown.d.ts +13 -0
  65. package/dist/formatting/markdown.d.ts.map +1 -0
  66. package/dist/index.d.ts +15 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +3967 -0
  69. package/dist/index.js.map +48 -0
  70. package/dist/logging.d.ts +10 -0
  71. package/dist/logging.d.ts.map +1 -0
  72. package/dist/protocol/compression.d.ts +8 -0
  73. package/dist/protocol/compression.d.ts.map +1 -0
  74. package/dist/protocol/enums.d.ts +175 -0
  75. package/dist/protocol/enums.d.ts.map +1 -0
  76. package/dist/protocol/index.d.ts +6 -0
  77. package/dist/protocol/index.d.ts.map +1 -0
  78. package/dist/protocol/index.js +438 -0
  79. package/dist/protocol/index.js.map +13 -0
  80. package/dist/protocol/tcp.d.ts +35 -0
  81. package/dist/protocol/tcp.d.ts.map +1 -0
  82. package/dist/protocol/types.d.ts +38 -0
  83. package/dist/protocol/types.d.ts.map +1 -0
  84. package/dist/protocol/websocket.d.ts +7 -0
  85. package/dist/protocol/websocket.d.ts.map +1 -0
  86. package/dist/session/index.d.ts +3 -0
  87. package/dist/session/index.d.ts.map +1 -0
  88. package/dist/session/index.js +179 -0
  89. package/dist/session/index.js.map +12 -0
  90. package/dist/session/store.d.ts +18 -0
  91. package/dist/session/store.d.ts.map +1 -0
  92. package/dist/session/types.d.ts +29 -0
  93. package/dist/session/types.d.ts.map +1 -0
  94. package/dist/telemetry/index.d.ts +4 -0
  95. package/dist/telemetry/index.d.ts.map +1 -0
  96. package/dist/telemetry/index.js +831 -0
  97. package/dist/telemetry/index.js.map +17 -0
  98. package/dist/telemetry/navigation.d.ts +41 -0
  99. package/dist/telemetry/navigation.d.ts.map +1 -0
  100. package/dist/telemetry/payloads.d.ts +30 -0
  101. package/dist/telemetry/payloads.d.ts.map +1 -0
  102. package/dist/telemetry/service.d.ts +53 -0
  103. package/dist/telemetry/service.d.ts.map +1 -0
  104. package/dist/transport/base.d.ts +18 -0
  105. package/dist/transport/base.d.ts.map +1 -0
  106. package/dist/transport/index.d.ts +4 -0
  107. package/dist/transport/index.d.ts.map +1 -0
  108. package/dist/transport/tcp.d.ts +28 -0
  109. package/dist/transport/tcp.d.ts.map +1 -0
  110. package/dist/transport/websocket.d.ts +14 -0
  111. package/dist/transport/websocket.d.ts.map +1 -0
  112. package/dist/types/attachments.d.ts +18 -0
  113. package/dist/types/attachments.d.ts.map +1 -0
  114. package/dist/types/index.d.ts +4 -0
  115. package/dist/types/index.d.ts.map +1 -0
  116. package/dist/types/index.js +376 -0
  117. package/dist/types/index.js.map +12 -0
  118. package/dist/types/models.d.ts +210 -0
  119. package/dist/types/models.d.ts.map +1 -0
  120. package/dist/types/utils.d.ts +10 -0
  121. package/dist/types/utils.d.ts.map +1 -0
  122. package/package.json +95 -0
  123. package/src/api/account.ts +93 -0
  124. package/src/api/auth.ts +312 -0
  125. package/src/api/bots.ts +21 -0
  126. package/src/api/chats.ts +234 -0
  127. package/src/api/facade.ts +28 -0
  128. package/src/api/index.ts +9 -0
  129. package/src/api/messages.ts +230 -0
  130. package/src/api/response.ts +24 -0
  131. package/src/api/runtime.ts +27 -0
  132. package/src/api/uploads.ts +91 -0
  133. package/src/api/users.ts +68 -0
  134. package/src/app.ts +271 -0
  135. package/src/auth/flows.ts +103 -0
  136. package/src/auth/index.ts +2 -0
  137. package/src/auth/providers.ts +30 -0
  138. package/src/client.ts +506 -0
  139. package/src/config.ts +161 -0
  140. package/src/connection/connection.ts +93 -0
  141. package/src/connection/index.ts +3 -0
  142. package/src/connection/pending.ts +49 -0
  143. package/src/connection/readers.ts +33 -0
  144. package/src/dispatch/dispatcher.ts +194 -0
  145. package/src/dispatch/index.ts +2 -0
  146. package/src/dispatch/router.ts +149 -0
  147. package/src/errors.ts +41 -0
  148. package/src/files/file.ts +92 -0
  149. package/src/files/index.ts +1 -0
  150. package/src/formatting/index.ts +1 -0
  151. package/src/formatting/markdown.ts +123 -0
  152. package/src/index.ts +15 -0
  153. package/src/logging.ts +67 -0
  154. package/src/protocol/compression.ts +69 -0
  155. package/src/protocol/enums.ts +175 -0
  156. package/src/protocol/index.ts +5 -0
  157. package/src/protocol/tcp.ts +139 -0
  158. package/src/protocol/types.ts +45 -0
  159. package/src/protocol/websocket.ts +47 -0
  160. package/src/session/index.ts +2 -0
  161. package/src/session/store.ts +154 -0
  162. package/src/session/types.ts +45 -0
  163. package/src/telemetry/index.ts +3 -0
  164. package/src/telemetry/navigation.ts +160 -0
  165. package/src/telemetry/payloads.ts +84 -0
  166. package/src/telemetry/service.ts +194 -0
  167. package/src/transport/base.ts +43 -0
  168. package/src/transport/index.ts +3 -0
  169. package/src/transport/tcp.ts +296 -0
  170. package/src/transport/websocket.ts +63 -0
  171. package/src/types/attachments.ts +24 -0
  172. package/src/types/index.ts +3 -0
  173. package/src/types/models.ts +461 -0
  174. package/src/types/utils.ts +45 -0
@@ -0,0 +1,234 @@
1
+ import { TsMaxError } from "../errors.ts";
2
+ import { Opcode } from "../protocol/index.ts";
3
+ import { Chat, Message, asRecord, type ChatActions, type MaxId } from "../types/index.ts";
4
+ import { listItem, payload, payloadItem } from "./response.ts";
5
+ import type { ApiRuntime } from "./runtime.ts";
6
+
7
+ export interface GroupSettings {
8
+ allCanPinMessage?: boolean | null;
9
+ onlyOwnerCanChangeIconTitle?: boolean | null;
10
+ onlyAdminCanAddMember?: boolean | null;
11
+ onlyAdminCanCall?: boolean | null;
12
+ membersCanSeePrivateLink?: boolean | null;
13
+ }
14
+
15
+ export class ChatApi implements ChatActions {
16
+ private readonly cache = new Map<MaxId, Chat>();
17
+
18
+ constructor(private readonly app: ApiRuntime) {}
19
+
20
+ seed(chats: Chat[]): void {
21
+ for (const chat of chats) this.cache.set(chat.id, this.app.bindChat(chat));
22
+ }
23
+
24
+ async createGroup(
25
+ name: string,
26
+ participantIds: MaxId[] = [],
27
+ notify = true,
28
+ ): Promise<[Chat, Message] | null> {
29
+ const frame = await this.app.invoke(Opcode.MSG_SEND, {
30
+ message: {
31
+ cid: BigInt(Date.now()),
32
+ attaches: [
33
+ {
34
+ _type: "CONTROL",
35
+ event: "new",
36
+ chatType: "CHAT",
37
+ title: name,
38
+ userIds: participantIds,
39
+ },
40
+ ],
41
+ },
42
+ notify,
43
+ });
44
+ const chatRaw = payloadItem(frame, "chat");
45
+ if (!chatRaw) return null;
46
+ const chat = this.cacheChat(Chat.fromPayload(chatRaw));
47
+ const data = payload(frame);
48
+ const message = this.app.bindMessage(Message.fromPayload(data.message ?? data));
49
+ return [chat, message];
50
+ }
51
+
52
+ async inviteUsersToGroup(
53
+ chatId: MaxId,
54
+ userIds: MaxId[],
55
+ showHistory = true,
56
+ ): Promise<Chat | null> {
57
+ return await this.memberAction(chatId, userIds, "add", showHistory);
58
+ }
59
+
60
+ async inviteUsersToChannel(
61
+ chatId: MaxId,
62
+ userIds: MaxId[],
63
+ showHistory = true,
64
+ ): Promise<Chat | null> {
65
+ return await this.inviteUsersToGroup(chatId, userIds, showHistory);
66
+ }
67
+
68
+ async removeUsersFromGroup(
69
+ chatId: MaxId,
70
+ userIds: MaxId[],
71
+ cleanMsgPeriod = 0,
72
+ ): Promise<Chat | null> {
73
+ const frame = await this.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, {
74
+ chatId,
75
+ userIds,
76
+ operation: "remove",
77
+ cleanMsgPeriod,
78
+ });
79
+ return this.optionalChat(payloadItem(frame, "chat"));
80
+ }
81
+
82
+ async changeGroupSettings(chatId: MaxId, settings: GroupSettings): Promise<void> {
83
+ const options: Record<string, boolean> = {};
84
+ const keys: Array<[keyof GroupSettings, string]> = [
85
+ ["allCanPinMessage", "ALL_CAN_PIN_MESSAGE"],
86
+ ["onlyOwnerCanChangeIconTitle", "ONLY_OWNER_CAN_CHANGE_ICON_TITLE"],
87
+ ["onlyAdminCanAddMember", "ONLY_ADMIN_CAN_ADD_MEMBER"],
88
+ ["onlyAdminCanCall", "ONLY_ADMIN_CAN_CALL"],
89
+ ["membersCanSeePrivateLink", "MEMBERS_CAN_SEE_PRIVATE_LINK"],
90
+ ];
91
+ for (const [source, target] of keys) {
92
+ const value = settings[source];
93
+ if (typeof value === "boolean") options[target] = value;
94
+ }
95
+ const frame = await this.app.invoke(Opcode.CHAT_UPDATE, { chatId, options });
96
+ this.optionalChat(payloadItem(frame, "chat"));
97
+ }
98
+
99
+ async changeGroupProfile(
100
+ chatId: MaxId,
101
+ name: string | null,
102
+ description: string | null = null,
103
+ ): Promise<void> {
104
+ const frame = await this.app.invoke(Opcode.CHAT_UPDATE, { chatId, theme: name, description });
105
+ this.optionalChat(payloadItem(frame, "chat"));
106
+ }
107
+
108
+ async joinGroup(link: string): Promise<Chat> {
109
+ const processed = processJoinLink(link);
110
+ if (!processed) throw new TypeError("Invalid group link");
111
+ return await this.join(processed);
112
+ }
113
+
114
+ async joinChannel(link: string): Promise<Chat> {
115
+ return await this.join(processJoinLink(link) ?? link);
116
+ }
117
+
118
+ async resolveGroupByLink(link: string): Promise<Chat | null> {
119
+ const processed = processJoinLink(link);
120
+ if (!processed) throw new TypeError("Invalid group link");
121
+ const frame = await this.app.invoke(Opcode.LINK_INFO, { link: processed });
122
+ return this.optionalChat(payloadItem(frame, "chat"));
123
+ }
124
+
125
+ async reworkInviteLink(chatId: MaxId): Promise<Chat> {
126
+ const frame = await this.app.invoke(Opcode.CHAT_UPDATE, { revokePrivateLink: true, chatId });
127
+ const chat = this.optionalChat(payloadItem(frame, "chat"));
128
+ if (!chat) throw new TsMaxError("Chat not found in response");
129
+ return chat;
130
+ }
131
+
132
+ async getChats(chatIds: MaxId[]): Promise<Chat[]> {
133
+ const missing = chatIds.filter((id) => !this.cache.has(id));
134
+ if (missing.length > 0) {
135
+ const frame = await this.app.invoke(Opcode.CHAT_INFO, { chatIds: missing });
136
+ for (const raw of listItem(frame, "chats")) this.cacheChat(Chat.fromPayload(raw));
137
+ }
138
+ return chatIds.flatMap((id) => {
139
+ const chat = this.cache.get(id);
140
+ return chat ? [chat] : [];
141
+ });
142
+ }
143
+
144
+ async getChat(chatId: MaxId): Promise<Chat> {
145
+ const chat = (await this.getChats([chatId]))[0];
146
+ if (!chat) throw new TsMaxError("Chat not found in response");
147
+ return chat;
148
+ }
149
+
150
+ async fetchChats(marker = Date.now()): Promise<Chat[]> {
151
+ const frame = await this.app.invoke(Opcode.CHATS_LIST, { marker });
152
+ return listItem(frame, "chats").map((raw) => this.cacheChat(Chat.fromPayload(raw)));
153
+ }
154
+
155
+ async leaveGroup(chatId: MaxId): Promise<void> {
156
+ await this.app.invoke(Opcode.CHAT_LEAVE, { chatId });
157
+ this.cache.delete(chatId);
158
+ }
159
+
160
+ async leaveChannel(chatId: MaxId): Promise<void> {
161
+ await this.leaveGroup(chatId);
162
+ }
163
+
164
+ async deleteChat(chatId: MaxId, forMe = true, lastEventTime = Date.now()): Promise<boolean> {
165
+ await this.app.invoke(Opcode.CHAT_DELETE, { chatId, lastEventTime, forAll: !forMe });
166
+ this.cache.delete(chatId);
167
+ return true;
168
+ }
169
+
170
+ async getJoinRequests(chatId: MaxId, count = 100): Promise<Record<string, unknown>[]> {
171
+ return listItem(
172
+ await this.app.invoke(Opcode.CHAT_MEMBERS, { chatId, type: "JOIN_REQUEST", count }),
173
+ "members",
174
+ ).map((item) => asRecord(item));
175
+ }
176
+
177
+ async confirmJoinRequests(
178
+ chatId: MaxId,
179
+ userIds: MaxId[],
180
+ showHistory = true,
181
+ ): Promise<Chat | null> {
182
+ return await this.memberAction(chatId, userIds, "add", showHistory, "JOIN_REQUEST");
183
+ }
184
+
185
+ async confirmJoinRequest(chatId: MaxId, userId: MaxId, showHistory = true): Promise<Chat | null> {
186
+ return await this.confirmJoinRequests(chatId, [userId], showHistory);
187
+ }
188
+
189
+ async declineJoinRequests(chatId: MaxId, userIds: MaxId[]): Promise<Chat | null> {
190
+ return await this.memberAction(chatId, userIds, "remove", null, "JOIN_REQUEST");
191
+ }
192
+
193
+ async declineJoinRequest(chatId: MaxId, userId: MaxId): Promise<Chat | null> {
194
+ return await this.declineJoinRequests(chatId, [userId]);
195
+ }
196
+
197
+ private async join(link: string): Promise<Chat> {
198
+ const frame = await this.app.invoke(Opcode.CHAT_JOIN, { link });
199
+ const chat = this.optionalChat(payloadItem(frame, "chat"));
200
+ if (!chat) throw new TsMaxError("Chat not found in response");
201
+ return chat;
202
+ }
203
+
204
+ private async memberAction(
205
+ chatId: MaxId,
206
+ userIds: MaxId[],
207
+ operation: "add" | "remove",
208
+ showHistory: boolean | null,
209
+ type?: string,
210
+ ): Promise<Chat | null> {
211
+ const frame = await this.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, {
212
+ chatId,
213
+ userIds,
214
+ operation,
215
+ showHistory,
216
+ ...(type ? { type } : {}),
217
+ });
218
+ return this.optionalChat(payloadItem(frame, "chat"));
219
+ }
220
+
221
+ private optionalChat(raw: unknown): Chat | null {
222
+ return raw ? this.cacheChat(Chat.fromPayload(raw)) : null;
223
+ }
224
+ private cacheChat(chat: Chat): Chat {
225
+ const bound = this.app.bindChat(chat);
226
+ this.cache.set(bound.id, bound);
227
+ return bound;
228
+ }
229
+ }
230
+
231
+ function processJoinLink(link: string): string | null {
232
+ const index = link.indexOf("join/");
233
+ return index >= 0 ? link.slice(index) : null;
234
+ }
@@ -0,0 +1,28 @@
1
+ import { AccountApi } from "./account.ts";
2
+ import { AuthApi } from "./auth.ts";
3
+ import { BotsApi } from "./bots.ts";
4
+ import { ChatApi } from "./chats.ts";
5
+ import { MessageApi } from "./messages.ts";
6
+ import type { ApiRuntime } from "./runtime.ts";
7
+ import { UploadApi } from "./uploads.ts";
8
+ import { UserApi } from "./users.ts";
9
+
10
+ export class ApiFacade {
11
+ readonly auth: AuthApi;
12
+ readonly uploads: UploadApi;
13
+ readonly messages: MessageApi;
14
+ readonly chats: ChatApi;
15
+ readonly users: UserApi;
16
+ readonly account: AccountApi;
17
+ readonly bots: BotsApi;
18
+
19
+ constructor(app: ApiRuntime) {
20
+ this.auth = new AuthApi(app);
21
+ this.uploads = new UploadApi(app);
22
+ this.messages = new MessageApi(app, this.uploads);
23
+ this.chats = new ChatApi(app);
24
+ this.users = new UserApi(app);
25
+ this.account = new AccountApi(app, this.uploads);
26
+ this.bots = new BotsApi(app);
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./account.ts";
2
+ export * from "./auth.ts";
3
+ export * from "./bots.ts";
4
+ export * from "./chats.ts";
5
+ export * from "./facade.ts";
6
+ export * from "./messages.ts";
7
+ export * from "./runtime.ts";
8
+ export * from "./uploads.ts";
9
+ export * from "./users.ts";
@@ -0,0 +1,230 @@
1
+ import type { SendAttachment } from "../files/index.ts";
2
+ import { formatMarkdown } from "../formatting/index.ts";
3
+ import { Opcode } from "../protocol/index.ts";
4
+ import {
5
+ Message,
6
+ asRecord,
7
+ parseReactionInfo,
8
+ parseReadState,
9
+ toMaxId,
10
+ type EditMessageOptions,
11
+ type ForwardMessageOptions,
12
+ type HistoryOptions,
13
+ type MaxId,
14
+ type MessageActions,
15
+ type ReactionInfo,
16
+ type ReadState,
17
+ type SendMessageOptions,
18
+ } from "../types/index.ts";
19
+ import { listItem, payload, payloadItem, requiredItem } from "./response.ts";
20
+ import type { ApiRuntime } from "./runtime.ts";
21
+ import type { UploadApi, UploadedAttachment } from "./uploads.ts";
22
+
23
+ export class MessageApi implements MessageActions {
24
+ private previousCid = Date.now();
25
+
26
+ constructor(
27
+ private readonly app: ApiRuntime,
28
+ private readonly uploads: UploadApi,
29
+ ) {}
30
+
31
+ async sendMessage(options: SendMessageOptions): Promise<Message | null> {
32
+ const formatted = formatMarkdown(options.text);
33
+ const attaches = await this.uploadAttachments(
34
+ options.attachments as readonly SendAttachment[] | null,
35
+ );
36
+ const link = options.replyTo ? { type: "REPLY", messageId: options.replyTo } : undefined;
37
+ const frame = await this.app.invoke(Opcode.MSG_SEND, {
38
+ chatId: options.chatId,
39
+ message: {
40
+ text: formatted.text,
41
+ cid: this.nextCid(),
42
+ elements: formatted.elements,
43
+ attaches,
44
+ ...(link ? { link } : {}),
45
+ },
46
+ notify: options.notify ?? true,
47
+ });
48
+ return this.parseMessage(payload(frame));
49
+ }
50
+
51
+ async forwardMessage(options: ForwardMessageOptions): Promise<Message | null> {
52
+ const frame = await this.app.invoke(Opcode.MSG_SEND, {
53
+ chatId: options.chatId,
54
+ message: {
55
+ cid: -this.nextCid(),
56
+ link: {
57
+ type: "FORWARD",
58
+ messageId: options.messageId.toString(),
59
+ chatId: options.sourceChatId ?? options.chatId,
60
+ },
61
+ attaches: [],
62
+ },
63
+ notify: options.notify ?? true,
64
+ });
65
+ return this.parseMessage(payload(frame));
66
+ }
67
+
68
+ async getMessages(chatId: MaxId, messageIds: MaxId[]): Promise<Message[]> {
69
+ const frame = await this.app.invoke(Opcode.MSG_GET, { chatId, messageIds });
70
+ return listItem(frame, "messages").map((item) => {
71
+ const message = this.app.bindMessage(Message.fromPayload(item));
72
+ message.chatId ??= chatId;
73
+ return message;
74
+ });
75
+ }
76
+
77
+ async getMessage(chatId: MaxId, messageId: MaxId): Promise<Message | null> {
78
+ return (await this.getMessages(chatId, [messageId]))[0] ?? null;
79
+ }
80
+
81
+ async editMessage(options: EditMessageOptions): Promise<Message> {
82
+ const formatted = formatMarkdown(options.text);
83
+ const frame = await this.app.invoke(Opcode.MSG_EDIT, {
84
+ chatId: options.chatId,
85
+ messageId: options.messageId,
86
+ text: formatted.text,
87
+ elements: formatted.elements,
88
+ attachments: await this.uploadAttachments(
89
+ options.attachments as readonly SendAttachment[] | null,
90
+ ),
91
+ });
92
+ const message = this.app.bindMessage(Message.fromPayload(requiredItem(frame, "message")));
93
+ message.chatId ??= options.chatId;
94
+ return message;
95
+ }
96
+
97
+ async fetchHistory(options: HistoryOptions): Promise<Message[] | null> {
98
+ const frame = await this.app.invoke(Opcode.CHAT_HISTORY, {
99
+ chatId: options.chatId,
100
+ forward: options.forward ?? 0,
101
+ backward: options.backward ?? 40,
102
+ backwardTime: options.backwardTime ?? 0,
103
+ forwardTime: options.forwardTime ?? 0,
104
+ from: options.from ?? Date.now(),
105
+ itemType: options.itemType ?? "REGULAR",
106
+ getChat: options.getChat ?? false,
107
+ getMessages: options.getMessages ?? true,
108
+ interactive: options.interactive ?? false,
109
+ });
110
+ const result = listItem(frame, "messages").map((item) =>
111
+ this.app.bindMessage(Message.fromPayload(item)),
112
+ );
113
+ return result.length > 0 ? result : null;
114
+ }
115
+
116
+ async deleteMessage(options: {
117
+ chatId: MaxId;
118
+ messageIds: MaxId[];
119
+ forMe: boolean;
120
+ }): Promise<boolean> {
121
+ await this.app.invoke(Opcode.MSG_DELETE, options);
122
+ return true;
123
+ }
124
+
125
+ async pinMessage(options: {
126
+ chatId: MaxId;
127
+ messageId: MaxId;
128
+ notifyPin: boolean;
129
+ }): Promise<boolean> {
130
+ await this.app.invoke(Opcode.CHAT_UPDATE, {
131
+ chatId: options.chatId,
132
+ notifyPin: options.notifyPin,
133
+ pinMessageId: options.messageId,
134
+ });
135
+ return true;
136
+ }
137
+
138
+ async readMessage(messageId: MaxId, chatId: MaxId): Promise<ReadState> {
139
+ const frame = await this.app.invoke(Opcode.CHAT_MARK, {
140
+ type: "READ_MESSAGE",
141
+ chatId,
142
+ messageId,
143
+ mark: Date.now(),
144
+ });
145
+ return parseReadState(payload(frame));
146
+ }
147
+
148
+ async addReaction(
149
+ chatId: MaxId,
150
+ messageId: MaxId,
151
+ reaction: string,
152
+ ): Promise<ReactionInfo | null> {
153
+ const frame = await this.app.invoke(Opcode.MSG_REACTION, {
154
+ chatId,
155
+ messageId: messageId.toString(),
156
+ reaction: { id: reaction },
157
+ });
158
+ return parseReactionInfo(payloadItem(frame, "reactionInfo"));
159
+ }
160
+
161
+ async removeReaction(chatId: MaxId, messageId: MaxId): Promise<ReactionInfo | null> {
162
+ const frame = await this.app.invoke(Opcode.MSG_CANCEL_REACTION, {
163
+ chatId,
164
+ messageId: messageId.toString(),
165
+ });
166
+ return parseReactionInfo(payloadItem(frame, "reactionInfo"));
167
+ }
168
+
169
+ async getReactions(chatId: MaxId, messageIds: MaxId[]): Promise<Map<MaxId, ReactionInfo> | null> {
170
+ const frame = await this.app.invoke(Opcode.MSG_GET_REACTIONS, {
171
+ chatId,
172
+ messageIds: messageIds.map(String),
173
+ });
174
+ const raw = payloadItem(frame, "messagesReactions");
175
+ if (!raw) return null;
176
+ const result = new Map<MaxId, ReactionInfo>();
177
+ for (const [id, info] of Object.entries(asRecord(raw))) {
178
+ const parsed = parseReactionInfo(info);
179
+ if (parsed) result.set(toMaxId(id), parsed);
180
+ }
181
+ return result;
182
+ }
183
+
184
+ async getVideoById(
185
+ chatId: MaxId,
186
+ messageId: MaxId,
187
+ videoId: number,
188
+ ): Promise<Record<string, unknown> | null> {
189
+ const frame = await this.app.invoke(Opcode.VIDEO_PLAY, {
190
+ chatId,
191
+ messageId: messageId.toString(),
192
+ videoId,
193
+ });
194
+ return frame.payload;
195
+ }
196
+
197
+ async getFileById(
198
+ chatId: MaxId,
199
+ messageId: MaxId,
200
+ fileId: number,
201
+ ): Promise<Record<string, unknown> | null> {
202
+ const frame = await this.app.invoke(Opcode.FILE_DOWNLOAD, {
203
+ chatId,
204
+ messageId: messageId.toString(),
205
+ fileId,
206
+ });
207
+ return frame.payload;
208
+ }
209
+
210
+ private nextCid(): bigint {
211
+ const next = Math.max(Date.now(), this.previousCid + 1);
212
+ this.previousCid = next;
213
+ return BigInt(next);
214
+ }
215
+
216
+ private async uploadAttachments(
217
+ attachments?: readonly SendAttachment[] | null,
218
+ ): Promise<UploadedAttachment[]> {
219
+ const result: UploadedAttachment[] = [];
220
+ for (const attachment of attachments ?? []) result.push(await this.uploads.upload(attachment));
221
+ return result;
222
+ }
223
+
224
+ private parseMessage(value: unknown): Message | null {
225
+ if (!value) return null;
226
+ const data = asRecord(value);
227
+ const candidate = data.message ?? value;
228
+ return this.app.bindMessage(Message.fromPayload(candidate));
229
+ }
230
+ }
@@ -0,0 +1,24 @@
1
+ import type { InboundFrame } from "../protocol/index.ts";
2
+ import { asArray, asRecord } from "../types/index.ts";
3
+
4
+ export function payload(frame: InboundFrame): Record<string, unknown> {
5
+ return frame.payload ?? {};
6
+ }
7
+
8
+ export function payloadItem(frame: InboundFrame, key: string): unknown {
9
+ return payload(frame)[key];
10
+ }
11
+
12
+ export function requiredItem(frame: InboundFrame, key: string): unknown {
13
+ const value = payloadItem(frame, key);
14
+ if (value === null || value === undefined) throw new TypeError(`Response is missing ${key}`);
15
+ return value;
16
+ }
17
+
18
+ export function recordItem(frame: InboundFrame, key?: string): Record<string, unknown> {
19
+ return asRecord(key ? requiredItem(frame, key) : payload(frame));
20
+ }
21
+
22
+ export function listItem(frame: InboundFrame, key: string): unknown[] {
23
+ return asArray(payloadItem(frame, key));
24
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResolvedConfig } from "../config.ts";
2
+ import type { InboundFrame } from "../protocol/index.ts";
3
+ import type { SessionInfo, SessionStore } from "../session/index.ts";
4
+ import type { Chat, Message, Profile, User } from "../types/index.ts";
5
+
6
+ export interface ApiRuntime {
7
+ readonly config: ResolvedConfig;
8
+ readonly store: SessionStore;
9
+ session: SessionInfo | null;
10
+ me: Profile | null;
11
+ invoke(
12
+ opcode: number,
13
+ payload?: Record<string, unknown>,
14
+ timeout?: number,
15
+ ): Promise<InboundFrame>;
16
+ bindMessage(message: Message): Message;
17
+ bindChat(chat: Chat): Chat;
18
+ bindUser(user: User): User;
19
+ createAttachmentWaiter?(
20
+ field: "fileId" | "videoId",
21
+ id: number,
22
+ timeoutSeconds?: number,
23
+ ): {
24
+ promise: Promise<void>;
25
+ cancel(): void;
26
+ };
27
+ }
@@ -0,0 +1,91 @@
1
+ import { UploadError } from "../errors.ts";
2
+ import { File, Photo, Video, type SendAttachment } from "../files/index.ts";
3
+ import { Opcode } from "../protocol/index.ts";
4
+ import { asArray, asNumber, asRecord, asString } from "../types/index.ts";
5
+ import { payload } from "./response.ts";
6
+ import type { ApiRuntime } from "./runtime.ts";
7
+
8
+ export type UploadedAttachment =
9
+ | { _type: "PHOTO"; photoToken: string }
10
+ | { _type: "VIDEO"; videoId: number; token: string }
11
+ | { _type: "FILE"; fileId: number };
12
+
13
+ export class UploadApi {
14
+ constructor(private readonly app: ApiRuntime) {}
15
+
16
+ async upload(attachment: SendAttachment): Promise<UploadedAttachment> {
17
+ if (attachment instanceof Photo) return await this.uploadPhoto(attachment);
18
+ if (attachment instanceof Video) return await this.uploadVideo(attachment);
19
+ if (attachment instanceof File) return await this.uploadFile(attachment);
20
+ throw new UploadError("Unsupported attachment");
21
+ }
22
+
23
+ async uploadPhoto(photo: Photo, profile = false): Promise<UploadedAttachment> {
24
+ const response = payload(await this.app.invoke(Opcode.PHOTO_UPLOAD, { count: 1, profile }));
25
+ const url = asString(response.url);
26
+ if (!url) throw new UploadError("No photo upload URL received");
27
+ const photoId = new URL(url).searchParams.get("photoIds");
28
+ if (!photoId) throw new UploadError("Photo upload URL has no photoIds");
29
+ const form = new FormData();
30
+ const bytes = await photo.read();
31
+ form.append(
32
+ "file",
33
+ new Blob([bytes], { type: photo.format.mimeType }),
34
+ `image.${photo.format.extension}`,
35
+ );
36
+ const upload = await fetch(url, { method: "POST", body: form });
37
+ if (!upload.ok) throw new UploadError(`Photo upload failed with status ${upload.status}`);
38
+ const result = asRecord(await upload.json(), "photo upload response");
39
+ const photos = asRecord(result.photos, "photos");
40
+ const item = asRecord(photos[photoId], "photo result");
41
+ const token = asString(item.token);
42
+ if (!token) throw new UploadError("Photo upload response has no token");
43
+ return { _type: "PHOTO", photoToken: token };
44
+ }
45
+
46
+ async uploadVideo(video: Video): Promise<UploadedAttachment> {
47
+ const response = payload(
48
+ await this.app.invoke(Opcode.VIDEO_UPLOAD, { count: 1, profile: false }),
49
+ );
50
+ const info = asRecord(asArray(response.info)[0], "video upload info");
51
+ const videoId = asNumber(info.videoId);
52
+ const token = asString(info.token);
53
+ const waiter = this.app.createAttachmentWaiter?.("videoId", videoId, 60);
54
+ try {
55
+ await this.postBinary(asString(info.url), video, true);
56
+ await waiter?.promise;
57
+ return { _type: "VIDEO", videoId, token };
58
+ } finally {
59
+ waiter?.cancel();
60
+ }
61
+ }
62
+
63
+ async uploadFile(file: File): Promise<UploadedAttachment> {
64
+ const response = payload(
65
+ await this.app.invoke(Opcode.FILE_UPLOAD, { count: 1, profile: false }),
66
+ );
67
+ const info = asRecord(asArray(response.info)[0], "file upload info");
68
+ const fileId = asNumber(info.fileId);
69
+ const waiter = this.app.createAttachmentWaiter?.("fileId", fileId, 60);
70
+ try {
71
+ await this.postBinary(asString(info.url), file, false);
72
+ await waiter?.promise;
73
+ return { _type: "FILE", fileId };
74
+ } finally {
75
+ waiter?.cancel();
76
+ }
77
+ }
78
+
79
+ private async postBinary(url: string, file: File | Video, contentRange: boolean): Promise<void> {
80
+ if (!url) throw new UploadError("No upload URL received");
81
+ const bytes = await file.read();
82
+ const headers: Record<string, string> = {
83
+ "Content-Disposition": `attachment; filename=${encodeURIComponent(file.name)}`,
84
+ "Content-Length": String(bytes.byteLength),
85
+ };
86
+ if (contentRange || bytes.byteLength > 0)
87
+ headers["Content-Range"] = `0-${bytes.byteLength - 1}/${bytes.byteLength}`;
88
+ const response = await fetch(url, { method: "POST", headers, body: bytes });
89
+ if (!response.ok) throw new UploadError(`Upload failed with status ${response.status}`);
90
+ }
91
+ }