@leemour/max-cli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -14
- package/dist/bot/auth.d.ts +23 -0
- package/dist/bot/auth.d.ts.map +1 -0
- package/dist/bot/auth.js +36 -0
- package/dist/bot/auth.js.map +1 -0
- package/dist/bot/client.d.ts +20 -0
- package/dist/bot/client.d.ts.map +1 -0
- package/dist/bot/client.js +91 -0
- package/dist/bot/client.js.map +1 -0
- package/dist/bot/generated/manifest.d.ts +3 -0
- package/dist/bot/generated/manifest.d.ts.map +1 -0
- package/dist/bot/generated/manifest.js +1232 -0
- package/dist/bot/generated/manifest.js.map +1 -0
- package/dist/bot/generated/schemas.d.ts +271 -0
- package/dist/bot/generated/schemas.d.ts.map +1 -0
- package/dist/bot/generated/schemas.js +869 -0
- package/dist/bot/generated/schemas.js.map +1 -0
- package/dist/bot/generated/types.d.ts +1088 -0
- package/dist/bot/generated/types.d.ts.map +1 -0
- package/dist/bot/generated/types.js +5 -0
- package/dist/bot/generated/types.js.map +1 -0
- package/dist/bot/input.d.ts +10 -0
- package/dist/bot/input.d.ts.map +1 -0
- package/dist/bot/input.js +88 -0
- package/dist/bot/input.js.map +1 -0
- package/dist/bot/keep.d.ts +25 -0
- package/dist/bot/keep.d.ts.map +1 -0
- package/dist/bot/keep.js +96 -0
- package/dist/bot/keep.js.map +1 -0
- package/dist/bot/map.d.ts +11 -0
- package/dist/bot/map.d.ts.map +1 -0
- package/dist/bot/map.js +76 -0
- package/dist/bot/map.js.map +1 -0
- package/dist/bot/permissions.d.ts +14 -0
- package/dist/bot/permissions.d.ts.map +1 -0
- package/dist/bot/permissions.js +53 -0
- package/dist/bot/permissions.js.map +1 -0
- package/dist/bot/registry.d.ts +30 -0
- package/dist/bot/registry.d.ts.map +1 -0
- package/dist/bot/registry.js +85 -0
- package/dist/bot/registry.js.map +1 -0
- package/dist/bot/russian-trusted-root.d.ts +8 -0
- package/dist/bot/russian-trusted-root.d.ts.map +1 -0
- package/dist/bot/russian-trusted-root.js +41 -0
- package/dist/bot/russian-trusted-root.js.map +1 -0
- package/dist/bot/transport.d.ts +41 -0
- package/dist/bot/transport.d.ts.map +1 -0
- package/dist/bot/transport.js +200 -0
- package/dist/bot/transport.js.map +1 -0
- package/dist/bot/uploads.d.ts +34 -0
- package/dist/bot/uploads.d.ts.map +1 -0
- package/dist/bot/uploads.js +122 -0
- package/dist/bot/uploads.js.map +1 -0
- package/dist/client.d.ts +39 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +168 -23
- package/dist/client.js.map +1 -1
- package/dist/commands/bot-comments.d.ts +4 -0
- package/dist/commands/bot-comments.d.ts.map +1 -0
- package/dist/commands/bot-comments.js +97 -0
- package/dist/commands/bot-comments.js.map +1 -0
- package/dist/commands/bot-context.d.ts +27 -0
- package/dist/commands/bot-context.d.ts.map +1 -0
- package/dist/commands/bot-context.js +82 -0
- package/dist/commands/bot-context.js.map +1 -0
- package/dist/commands/bot-members.d.ts +12 -0
- package/dist/commands/bot-members.d.ts.map +1 -0
- package/dist/commands/bot-members.js +136 -0
- package/dist/commands/bot-members.js.map +1 -0
- package/dist/commands/bot-reads.d.ts +7 -0
- package/dist/commands/bot-reads.d.ts.map +1 -0
- package/dist/commands/bot-reads.js +145 -0
- package/dist/commands/bot-reads.js.map +1 -0
- package/dist/commands/bot-sends.d.ts +17 -0
- package/dist/commands/bot-sends.d.ts.map +1 -0
- package/dist/commands/bot-sends.js +270 -0
- package/dist/commands/bot-sends.js.map +1 -0
- package/dist/commands/bot-setup.d.ts +5 -0
- package/dist/commands/bot-setup.d.ts.map +1 -0
- package/dist/commands/bot-setup.js +107 -0
- package/dist/commands/bot-setup.js.map +1 -0
- package/dist/commands/bot-updates.d.ts +3 -0
- package/dist/commands/bot-updates.d.ts.map +1 -0
- package/dist/commands/bot-updates.js +182 -0
- package/dist/commands/bot-updates.js.map +1 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.d.ts.map +1 -0
- package/dist/commands/bot.js +160 -0
- package/dist/commands/bot.js.map +1 -0
- package/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +79 -2
- package/dist/commands/chats.js.map +1 -1
- package/dist/commands/check.d.ts +8 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +64 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/context.d.ts +15 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +5 -1
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/hearing.d.ts +25 -0
- package/dist/commands/hearing.d.ts.map +1 -0
- package/dist/commands/hearing.js +41 -0
- package/dist/commands/hearing.js.map +1 -0
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +19 -3
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +7 -4
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +21 -5
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/paging.d.ts +3 -1
- package/dist/commands/paging.d.ts.map +1 -1
- package/dist/commands/paging.js +4 -1
- package/dist/commands/paging.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +22 -8
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/rules.d.ts +7 -0
- package/dist/commands/rules.d.ts.map +1 -0
- package/dist/commands/rules.js +69 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/domain/map.d.ts +6 -1
- package/dist/domain/map.d.ts.map +1 -1
- package/dist/domain/map.js +34 -1
- package/dist/domain/map.js.map +1 -1
- package/dist/domain/models.d.ts +55 -0
- package/dist/domain/models.d.ts.map +1 -1
- package/dist/domain/models.js.map +1 -1
- package/dist/generated/client.generated.d.ts +1 -0
- package/dist/generated/client.generated.d.ts.map +1 -1
- package/dist/generated/client.generated.js +1 -0
- package/dist/generated/client.generated.js.map +1 -1
- package/dist/generated/opcodes.generated.d.ts +1 -0
- package/dist/generated/opcodes.generated.d.ts.map +1 -1
- package/dist/generated/opcodes.generated.js +1 -0
- package/dist/generated/opcodes.generated.js.map +1 -1
- package/dist/generated/operations.generated.d.ts +16 -1
- package/dist/generated/operations.generated.d.ts.map +1 -1
- package/dist/generated/operations.generated.js +2 -1
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/mcp/instructions.d.ts +2 -1
- package/dist/mcp/instructions.d.ts.map +1 -1
- package/dist/mcp/instructions.js +2 -1
- package/dist/mcp/instructions.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +4 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +5 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +160 -13
- package/dist/mcp/tools.js.map +1 -1
- package/dist/moderation/check.d.ts +98 -0
- package/dist/moderation/check.d.ts.map +1 -0
- package/dist/moderation/check.js +306 -0
- package/dist/moderation/check.js.map +1 -0
- package/dist/moderation/rules.d.ts +56 -0
- package/dist/moderation/rules.d.ts.map +1 -0
- package/dist/moderation/rules.js +138 -0
- package/dist/moderation/rules.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +18 -3
- package/dist/program.js.map +1 -1
- package/dist/protocol/connection.d.ts.map +1 -1
- package/dist/protocol/connection.js +12 -1
- package/dist/protocol/connection.js.map +1 -1
- package/dist/review.d.ts +32 -5
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +82 -45
- package/dist/review.js.map +1 -1
- package/dist/sends/journal.d.ts +1 -1
- package/dist/sends/journal.d.ts.map +1 -1
- package/dist/session/store.d.ts +2 -0
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +2 -0
- package/dist/session/store.js.map +1 -1
- package/dist/spec/index.d.ts.map +1 -1
- package/dist/spec/index.js +2 -1
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/operations/chats.d.ts +4 -1
- package/dist/spec/operations/chats.d.ts.map +1 -1
- package/dist/spec/operations/chats.js +10 -4
- package/dist/spec/operations/chats.js.map +1 -1
- package/dist/spec/operations/messages.d.ts.map +1 -1
- package/dist/spec/operations/messages.js +9 -4
- package/dist/spec/operations/messages.js.map +1 -1
- package/dist/spec/operations/uploads.d.ts +13 -0
- package/dist/spec/operations/uploads.d.ts.map +1 -1
- package/dist/spec/operations/uploads.js +21 -0
- package/dist/spec/operations/uploads.js.map +1 -1
- package/dist/transcribe/index.d.ts +36 -1
- package/dist/transcribe/index.d.ts.map +1 -1
- package/dist/transcribe/index.js +91 -14
- package/dist/transcribe/index.js.map +1 -1
- package/dist/upload.d.ts +3 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +13 -7
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/voice.d.ts +9 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +36 -0
- package/dist/voice.js.map +1 -0
- package/package.json +17 -6
- package/skills/max-cli/SKILL.md +23 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@leemour/cli-core";
|
|
2
|
-
import { namesFrom, SETTING_FLAGS, toChat, toContact, toFolder, toGroupCard, toMessage, toProfile, toReactions, toSession, } from "./domain/map.js";
|
|
2
|
+
import { namesFrom, SETTING_FLAGS, toChat, toContact, toFolder, toGroupCard, toGroupMember, toMessage, toProfile, toReactions, toSession, } from "./domain/map.js";
|
|
3
3
|
import { heldWindows } from "./export.js";
|
|
4
4
|
import { wireClient } from "./generated/client.generated.js";
|
|
5
5
|
import { parseMarkdown } from "./markdown.js";
|
|
@@ -14,7 +14,8 @@ import { loginPausedUntil, withLoginRefused, withoutLoginPause, } from "./sessio
|
|
|
14
14
|
import { WEB_USER_AGENT } from "./spec/identity.js";
|
|
15
15
|
import { buildRequest, checkResponse } from "./spec/index.js";
|
|
16
16
|
import { ASSET_TYPES } from "./spec/operations/assets.js";
|
|
17
|
-
import { isImage, readUpload, uploadFile, uploadPhoto } from "./upload.js";
|
|
17
|
+
import { isImage, isVideo, readUpload, uploadFile, uploadMedia, uploadPhoto } from "./upload.js";
|
|
18
|
+
import { voiceOf } from "./voice.js";
|
|
18
19
|
/**
|
|
19
20
|
* **The only thing above this line that knows MAX exists.** Commands speak the domain model; the
|
|
20
21
|
* opcodes and the frames stop here, so a different transport underneath changes this file and
|
|
@@ -202,6 +203,69 @@ export class MaxClient {
|
|
|
202
203
|
const raw = asArray(this.#session().chats).find((chat) => asId(chat.id) === chatId);
|
|
203
204
|
return raw ? this.#partnerOf(raw) : undefined;
|
|
204
205
|
},
|
|
206
|
+
/**
|
|
207
|
+
* The owner and admins of a group, from the chat as the login carried it (`owner`, `admins`,
|
|
208
|
+
* `adminParticipants` — measured 2026-09-24, `FIND-116`). `undefined` when the login did not
|
|
209
|
+
* carry this chat or its admins: the login carries only chats that changed lately, and the rest
|
|
210
|
+
* come from the local copy.
|
|
211
|
+
*/
|
|
212
|
+
adminIds: async (chatId) => {
|
|
213
|
+
const roles = await this.#roles(chatId);
|
|
214
|
+
return roles && [...new Set([...(roles.owner ? [roles.owner] : []), ...roles.admins])];
|
|
215
|
+
},
|
|
216
|
+
/**
|
|
217
|
+
* Who joined, left, was added or removed since a point: the service messages in the chat's
|
|
218
|
+
* history, read forward from `since` without reactions. At most `EVENTS_READ` messages, the oldest.
|
|
219
|
+
*/
|
|
220
|
+
events: async (reference, { since = Date.now() - EVENTS_DAYS * 86_400_000 } = {}) => {
|
|
221
|
+
if (this.#offline)
|
|
222
|
+
throw new CliError("validation_error", "`--offline` has no history to read events from");
|
|
223
|
+
const chatId = await this.chats.resolve(reference);
|
|
224
|
+
const { messages: read, more } = await this.chats.since(chatId, since);
|
|
225
|
+
const found = read.flatMap((message) => message.attachments
|
|
226
|
+
.filter((attachment) => attachment.kind === "control" && attachment.event !== undefined)
|
|
227
|
+
.map((attachment) => ({ message, attachment })));
|
|
228
|
+
const contacts = namesFrom(this.#session().contacts);
|
|
229
|
+
const ids = [
|
|
230
|
+
...new Set([
|
|
231
|
+
...found.flatMap(({ attachment }) => attachment.userIds ?? []),
|
|
232
|
+
...found.flatMap(({ message }) => message.senderName === null && message.senderId ? [message.senderId] : []),
|
|
233
|
+
]),
|
|
234
|
+
];
|
|
235
|
+
const names = await this.#namesOf(ids.filter((id) => !contacts.has(id)));
|
|
236
|
+
const nameOf = (id) => contacts.get(id) ?? names.get(id) ?? null;
|
|
237
|
+
return {
|
|
238
|
+
chatId,
|
|
239
|
+
since: new Date(since).toISOString(),
|
|
240
|
+
more,
|
|
241
|
+
events: found.map(({ message, attachment }) => ({
|
|
242
|
+
messageId: message.id,
|
|
243
|
+
timestamp: message.timestamp,
|
|
244
|
+
event: attachment.event ?? "",
|
|
245
|
+
by: { id: message.senderId, name: message.senderName ?? (message.senderId && nameOf(message.senderId)) },
|
|
246
|
+
people: (attachment.userIds ?? []).map((id) => ({ id, name: nameOf(id) })),
|
|
247
|
+
...(attachment.title ? { title: attachment.title } : {}),
|
|
248
|
+
})),
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* A chat's history after `since`, oldest first and without reactions, at most `EVENTS_READ`
|
|
253
|
+
* messages; `more` when there was more than that.
|
|
254
|
+
*/
|
|
255
|
+
since: async (chatId, since) => {
|
|
256
|
+
const read = [];
|
|
257
|
+
let from = since;
|
|
258
|
+
while (true) {
|
|
259
|
+
const page = (await this.#history(chatId, { from, backward: 0, forward: REVIEW_PAGE + 1 }, { reactions: false })).filter((message) => Date.parse(message.timestamp) > from);
|
|
260
|
+
read.push(...page);
|
|
261
|
+
const last = page.at(-1);
|
|
262
|
+
if (page.length < REVIEW_PAGE || !last)
|
|
263
|
+
return { messages: read, more: false };
|
|
264
|
+
if (read.length >= EVENTS_READ)
|
|
265
|
+
return { messages: read, more: true };
|
|
266
|
+
from = Date.parse(last.timestamp);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
205
269
|
/** What a link leads to, without joining it. */
|
|
206
270
|
inspect: async (link) => {
|
|
207
271
|
if (this.#offline)
|
|
@@ -271,6 +335,43 @@ export class MaxClient {
|
|
|
271
335
|
}, userIds);
|
|
272
336
|
},
|
|
273
337
|
members: {
|
|
338
|
+
/**
|
|
339
|
+
* Everyone in a group or channel, from MAX, page by page. Paging follows PyMax: each answer's
|
|
340
|
+
* `marker` asks for the next page, and none means the last. Stops at `MEMBERS_READ`, or when a
|
|
341
|
+
* page repeats its marker or brings nobody new, so a misread marker cannot loop.
|
|
342
|
+
*/
|
|
343
|
+
list: async (reference) => {
|
|
344
|
+
if (this.#offline)
|
|
345
|
+
throw new CliError("validation_error", "`--offline` has no member list; MAX has");
|
|
346
|
+
const chatId = await this.chats.resolve(reference);
|
|
347
|
+
await this.#connectOnce();
|
|
348
|
+
const members = new Map();
|
|
349
|
+
let marker = 0;
|
|
350
|
+
let complete = false;
|
|
351
|
+
while (members.size < MEMBERS_READ) {
|
|
352
|
+
const answer = await this.#wire.chats.members({ chatId, type: "MEMBER", marker, count: MEMBERS_PAGE });
|
|
353
|
+
const before = members.size;
|
|
354
|
+
for (const raw of asArray(answer.members)) {
|
|
355
|
+
const member = toGroupMember(raw);
|
|
356
|
+
if (member.id)
|
|
357
|
+
members.set(member.id, member);
|
|
358
|
+
}
|
|
359
|
+
const next = typeof answer.marker === "number" ? answer.marker : 0;
|
|
360
|
+
if (!next || next === marker || members.size === before) {
|
|
361
|
+
complete = !next;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
marker = next;
|
|
365
|
+
}
|
|
366
|
+
const roles = await this.#roles(chatId);
|
|
367
|
+
const role = (id) => (id === roles?.owner ? "owner" : roles?.admins.has(id) ? "admin" : "member");
|
|
368
|
+
return {
|
|
369
|
+
chatId,
|
|
370
|
+
members: [...members.values()].map((member) => (roles ? { ...member, role: role(member.id) } : member)),
|
|
371
|
+
complete,
|
|
372
|
+
rolesKnown: roles !== undefined,
|
|
373
|
+
};
|
|
374
|
+
},
|
|
274
375
|
/** No history unless asked (`NEED-272`): what was said before somebody joined is not theirs by default. */
|
|
275
376
|
add: (reference, people, { history = false } = {}) => this.#updateMembers(reference, people, "members.add", { operation: "add", showHistory: history }),
|
|
276
377
|
remove: (reference, people) => this.#updateMembers(reference, people, "members.remove", { operation: "remove", cleanMsgPeriod: 0 }),
|
|
@@ -284,13 +385,14 @@ export class MaxClient {
|
|
|
284
385
|
remove: (reference, person) => this.#updateMembers(reference, [person], "admins.remove", { operation: "remove", type: "ADMIN" }),
|
|
285
386
|
},
|
|
286
387
|
requests: {
|
|
388
|
+
/** Each with the age of their account, read like a member's — the answer's `{contact}` is the same shape. */
|
|
287
389
|
list: async (reference) => {
|
|
288
390
|
if (this.#offline)
|
|
289
391
|
throw new CliError("validation_error", "`--offline` reads what was recorded; join requests never are");
|
|
290
392
|
const chatId = await this.chats.resolve(reference);
|
|
291
393
|
await this.#connectOnce();
|
|
292
394
|
const answer = await this.#wire.chats.members({ chatId, type: "JOIN_REQUEST", count: JOIN_REQUESTS });
|
|
293
|
-
return asArray(answer.members).map(
|
|
395
|
+
return asArray(answer.members).map(toGroupMember);
|
|
294
396
|
},
|
|
295
397
|
accept: (reference, people) => this.#updateMembers(reference, people, "requests.accept", {
|
|
296
398
|
operation: "add",
|
|
@@ -693,6 +795,10 @@ export class MaxClient {
|
|
|
693
795
|
send: async (chatId, text, options = {}) => {
|
|
694
796
|
if (this.#offline)
|
|
695
797
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot send");
|
|
798
|
+
if (options.voice !== undefined && (text !== "" || (options.files ?? []).length > 0)) {
|
|
799
|
+
// MAX kept a caption beside a voice message (measured 2026-09-27), but web.max.ru never sends one; §34.
|
|
800
|
+
throw new CliError("validation_error", "a voice message goes alone — no text and no --file beside it");
|
|
801
|
+
}
|
|
696
802
|
if (options.at !== undefined && options.notify === false) {
|
|
697
803
|
// The web client always sends a scheduled message with `notify: true`; §34.
|
|
698
804
|
throw new CliError("validation_error", "a scheduled message cannot be silent — MAX's own client never sends one");
|
|
@@ -704,11 +810,16 @@ export class MaxClient {
|
|
|
704
810
|
const files = await Promise.all((options.files ?? []).map(async (path) => ({
|
|
705
811
|
path,
|
|
706
812
|
bytes: await readUpload(path, { anyFile: options.anyFile === true }),
|
|
707
|
-
|
|
813
|
+
kind: isImage(path) ? "photo" : isVideo(path) && options.asFile !== true ? "video" : "file",
|
|
708
814
|
})));
|
|
815
|
+
if (options.voice !== undefined) {
|
|
816
|
+
const bytes = await readUpload(options.voice, { anyFile: options.anyFile === true });
|
|
817
|
+
files.push({ path: options.voice, bytes, kind: "voice", voice: await voiceOf(options.voice, bytes) });
|
|
818
|
+
}
|
|
709
819
|
// Measured 2026-09-24: photos share a message, but a file with anything beside it is refused `proto.payload`.
|
|
710
|
-
|
|
711
|
-
|
|
820
|
+
// A video is kept alone the same way; a mixed message was never measured.
|
|
821
|
+
if (files.some((file) => file.kind !== "photo") && files.length > 1) {
|
|
822
|
+
throw new CliError("validation_error", "a file or a video goes in a message of its own — photos can share one; send them apart");
|
|
712
823
|
}
|
|
713
824
|
// Before connecting: a refused send never opens a socket when the chat was given as an id.
|
|
714
825
|
try {
|
|
@@ -724,10 +835,7 @@ export class MaxClient {
|
|
|
724
835
|
throw error;
|
|
725
836
|
}
|
|
726
837
|
const cid = options.cid ?? this.#nextCid();
|
|
727
|
-
const attachments = files.map(({ bytes,
|
|
728
|
-
kind: photo ? "photo" : "file",
|
|
729
|
-
bytes: bytes.length,
|
|
730
|
-
}));
|
|
838
|
+
const attachments = files.map(({ bytes, kind }) => ({ kind, bytes: bytes.length }));
|
|
731
839
|
const summary = {
|
|
732
840
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
733
841
|
...(options.at === undefined ? {} : { scheduledFor: new Date(options.at).toISOString() }),
|
|
@@ -1003,8 +1111,9 @@ export class MaxClient {
|
|
|
1003
1111
|
* forward from `since`; a chat with more than `REVIEW_PER_CHAT` in the window is cut short and
|
|
1004
1112
|
* says so.
|
|
1005
1113
|
*/
|
|
1006
|
-
review: async ({ since }) => {
|
|
1007
|
-
const { chats, changed, cut } = await this.#changedSince(since);
|
|
1114
|
+
review: async ({ since, chatId, }) => {
|
|
1115
|
+
const { chats, changed: all, cut } = await this.#changedSince(since);
|
|
1116
|
+
const changed = chatId === undefined ? all : all.filter((chat) => chat.id === chatId);
|
|
1008
1117
|
const { read, skipped } = capped(changed, REVIEW_CHATS);
|
|
1009
1118
|
const found = [];
|
|
1010
1119
|
for (const { id, title, kind } of read) {
|
|
@@ -1594,6 +1703,32 @@ export class MaxClient {
|
|
|
1594
1703
|
const ids = [...new Set([...messages, ...quoted].filter(needsName).map((message) => message.senderId))];
|
|
1595
1704
|
if (ids.length === 0)
|
|
1596
1705
|
return messages;
|
|
1706
|
+
const names = await this.#namesOf(ids);
|
|
1707
|
+
const name = (message) => needsName(message) && names.has(message.senderId)
|
|
1708
|
+
? { ...message, senderName: names.get(message.senderId) ?? null }
|
|
1709
|
+
: message;
|
|
1710
|
+
return messages.map((message) => ({
|
|
1711
|
+
...name(message),
|
|
1712
|
+
replyTo: message.replyTo && name(message.replyTo),
|
|
1713
|
+
forwardedFrom: message.forwardedFrom && name(message.forwardedFrom),
|
|
1714
|
+
}));
|
|
1715
|
+
}
|
|
1716
|
+
/** The chat's owner and admins as the login carried them; `undefined` when it did not. */
|
|
1717
|
+
async #roles(chatId) {
|
|
1718
|
+
await this.#connectOnce();
|
|
1719
|
+
const raw = asArray(this.#session().chats).find((chat) => asId(chat.id) === chatId);
|
|
1720
|
+
if (!raw || (raw.owner === undefined && raw.admins === undefined && raw.adminParticipants === undefined)) {
|
|
1721
|
+
return undefined;
|
|
1722
|
+
}
|
|
1723
|
+
const admins = [
|
|
1724
|
+
...(Array.isArray(raw.admins) ? raw.admins.map(asId) : []),
|
|
1725
|
+
...Object.keys(record(raw.adminParticipants) ?? {}),
|
|
1726
|
+
].filter((id) => id !== undefined);
|
|
1727
|
+
const owner = asId(raw.owner);
|
|
1728
|
+
return { ...(owner ? { owner } : {}), admins: new Set(admins.filter((id) => id !== owner)) };
|
|
1729
|
+
}
|
|
1730
|
+
/** Names we hold first, the rest from `CONTACT_INFO`, kept for next time. A refusal costs the names only. */
|
|
1731
|
+
async #namesOf(ids) {
|
|
1597
1732
|
const names = this.#cache?.people.names(ids) ?? new Map();
|
|
1598
1733
|
const fetched = [];
|
|
1599
1734
|
try {
|
|
@@ -1609,18 +1744,11 @@ export class MaxClient {
|
|
|
1609
1744
|
}
|
|
1610
1745
|
}
|
|
1611
1746
|
catch (error) {
|
|
1612
|
-
this.#warnAbout("names_unread", `some
|
|
1747
|
+
this.#warnAbout("names_unread", `some people are shown by id: their names could not be looked up (${reasonOf(error)})`);
|
|
1613
1748
|
}
|
|
1614
1749
|
if (fetched.length > 0)
|
|
1615
1750
|
this.#cache?.people.upsert(fetched, "info");
|
|
1616
|
-
|
|
1617
|
-
? { ...message, senderName: names.get(message.senderId) ?? null }
|
|
1618
|
-
: message;
|
|
1619
|
-
return messages.map((message) => ({
|
|
1620
|
-
...name(message),
|
|
1621
|
-
replyTo: message.replyTo && name(message.replyTo),
|
|
1622
|
-
forwardedFrom: message.forwardedFrom && name(message.forwardedFrom),
|
|
1623
|
-
}));
|
|
1751
|
+
return names;
|
|
1624
1752
|
}
|
|
1625
1753
|
/** INIT with the profile's own device, which is the device MAX then issues the token to (bite 8). */
|
|
1626
1754
|
async #beforeLogin(flow) {
|
|
@@ -1728,14 +1856,25 @@ export class MaxClient {
|
|
|
1728
1856
|
* Uploads one file and answers what the message attaches (measured 2026-09-24). An upload is never
|
|
1729
1857
|
* retried: a failure here happens before `MSG_SEND`, so nothing was sent.
|
|
1730
1858
|
*/
|
|
1731
|
-
async #upload({ path, bytes,
|
|
1859
|
+
async #upload({ path, bytes, kind, voice }) {
|
|
1732
1860
|
const request = { count: 1, type: 0, uploaderType: 0, profile: false };
|
|
1733
|
-
if (photo) {
|
|
1861
|
+
if (kind === "photo") {
|
|
1734
1862
|
const { url } = await this.#wire.uploads.photo(request);
|
|
1735
1863
|
if (typeof url !== "string")
|
|
1736
1864
|
throw new CliError("provider_error", "MAX gave no address to upload the photo to");
|
|
1737
1865
|
return { _type: "PHOTO", photoToken: await uploadPhoto(url, path, bytes) };
|
|
1738
1866
|
}
|
|
1867
|
+
if (kind === "video" || kind === "voice") {
|
|
1868
|
+
const slot = voice ? { ...request, type: 2, uploaderType: 1 } : request;
|
|
1869
|
+
const info = record(asArray((await this.#wire.uploads.video(slot)).info)[0]) ?? {};
|
|
1870
|
+
if (typeof info.url !== "string" || info.videoId === undefined) {
|
|
1871
|
+
throw new CliError("provider_error", `MAX gave no address to upload the ${kind} to`);
|
|
1872
|
+
}
|
|
1873
|
+
await uploadMedia(voice ? "voice message" : "video", info.url, path, bytes);
|
|
1874
|
+
return voice
|
|
1875
|
+
? { _type: "AUDIO", audioId: info.videoId, duration: voice.durationMs, wave: voice.wave, token: info.token }
|
|
1876
|
+
: { _type: "VIDEO", videoId: info.videoId, token: info.token, videoType: 0 };
|
|
1877
|
+
}
|
|
1739
1878
|
const info = record(asArray((await this.#wire.uploads.file(request)).info)[0]) ?? {};
|
|
1740
1879
|
if (typeof info.url !== "string" || info.fileId === undefined) {
|
|
1741
1880
|
throw new CliError("provider_error", "MAX gave no address to upload the file to");
|
|
@@ -1987,6 +2126,9 @@ export const ADMIN_RIGHTS = {
|
|
|
1987
2126
|
/** The most chats `max serve` holds from pushes before it logs in again instead. */
|
|
1988
2127
|
const LIVE_CHATS = 10_000;
|
|
1989
2128
|
const JOIN_REQUESTS = 100;
|
|
2129
|
+
/** PyMax's page, the one measured. */
|
|
2130
|
+
const MEMBERS_PAGE = 50;
|
|
2131
|
+
const MEMBERS_READ = 5000;
|
|
1990
2132
|
/**
|
|
1991
2133
|
* A private link goes as `join/<token>`, whatever came before it, as PyMax sends it and as
|
|
1992
2134
|
* measured. A public one (`https://max.ru/<name>`) goes whole, which is PyMax's claim only.
|
|
@@ -2053,6 +2195,9 @@ const INBOX_CHATS = 20;
|
|
|
2053
2195
|
const REVIEW_CHATS = 50;
|
|
2054
2196
|
const REVIEW_PAGE = 100;
|
|
2055
2197
|
const REVIEW_PER_CHAT = 500;
|
|
2198
|
+
const EVENTS_READ = 2000;
|
|
2199
|
+
/** How far back `chats events` looks without `since`. */
|
|
2200
|
+
export const EVENTS_DAYS = 7;
|
|
2056
2201
|
export const FIRST_TAB_SYNC = { folders: 0, calls: 0, assets: {} };
|
|
2057
2202
|
const numberOr = (value, fallback) => (typeof value === "number" ? value : fallback);
|
|
2058
2203
|
const eventTime = (chat) => {
|