@leemour/max-cli 0.14.0 → 0.16.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 +43 -9
- package/dist/bot/client.d.ts +7 -1
- package/dist/bot/client.d.ts.map +1 -1
- package/dist/bot/client.js +19 -5
- package/dist/bot/client.js.map +1 -1
- package/dist/bot/input.d.ts.map +1 -1
- package/dist/bot/input.js +7 -4
- package/dist/bot/input.js.map +1 -1
- package/dist/bot/joins.d.ts +26 -0
- package/dist/bot/joins.d.ts.map +1 -0
- package/dist/bot/joins.js +56 -0
- package/dist/bot/joins.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 +97 -0
- package/dist/bot/keep.js.map +1 -0
- package/dist/bot/map.d.ts +5 -2
- package/dist/bot/map.d.ts.map +1 -1
- package/dist/bot/map.js +7 -1
- package/dist/bot/map.js.map +1 -1
- package/dist/bot/registry.d.ts +3 -0
- package/dist/bot/registry.d.ts.map +1 -1
- package/dist/bot/registry.js +27 -10
- package/dist/bot/registry.js.map +1 -1
- 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/bot-mcp/instructions.d.ts +9 -0
- package/dist/bot-mcp/instructions.d.ts.map +1 -0
- package/dist/bot-mcp/instructions.js +22 -0
- package/dist/bot-mcp/instructions.js.map +1 -0
- package/dist/bot-mcp/server.d.ts +21 -0
- package/dist/bot-mcp/server.d.ts.map +1 -0
- package/dist/bot-mcp/server.js +199 -0
- package/dist/bot-mcp/server.js.map +1 -0
- package/dist/bot-mcp/tools.d.ts +27 -0
- package/dist/bot-mcp/tools.d.ts.map +1 -0
- package/dist/bot-mcp/tools.js +280 -0
- package/dist/bot-mcp/tools.js.map +1 -0
- package/dist/client.d.ts +44 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +232 -41
- package/dist/client.js.map +1 -1
- package/dist/commands/bot-check.d.ts +11 -0
- package/dist/commands/bot-check.d.ts.map +1 -0
- package/dist/commands/bot-check.js +206 -0
- package/dist/commands/bot-check.js.map +1 -0
- 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 +9 -3
- package/dist/commands/bot-context.d.ts.map +1 -1
- package/dist/commands/bot-context.js +38 -15
- package/dist/commands/bot-context.js.map +1 -1
- package/dist/commands/bot-mcp.d.ts +3 -0
- package/dist/commands/bot-mcp.d.ts.map +1 -0
- package/dist/commands/bot-mcp.js +69 -0
- package/dist/commands/bot-mcp.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-people.d.ts +10 -0
- package/dist/commands/bot-people.d.ts.map +1 -0
- package/dist/commands/bot-people.js +156 -0
- package/dist/commands/bot-people.js.map +1 -0
- package/dist/commands/bot-reads.d.ts.map +1 -1
- package/dist/commands/bot-reads.js +56 -8
- package/dist/commands/bot-reads.js.map +1 -1
- package/dist/commands/bot-sends.d.ts +5 -0
- package/dist/commands/bot-sends.d.ts.map +1 -1
- package/dist/commands/bot-sends.js +56 -11
- package/dist/commands/bot-sends.js.map +1 -1
- 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 +196 -0
- package/dist/commands/bot-updates.js.map +1 -0
- package/dist/commands/bot.d.ts.map +1 -1
- package/dist/commands/bot.js +21 -1
- package/dist/commands/bot.js.map +1 -1
- package/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +19 -6
- 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 +5 -0
- 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/polls.d.ts +3 -0
- package/dist/commands/polls.d.ts.map +1 -0
- package/dist/commands/polls.js +103 -0
- package/dist/commands/polls.js.map +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +5 -7
- package/dist/commands/review.js.map +1 -1
- package/dist/domain/map.d.ts +9 -1
- package/dist/domain/map.d.ts.map +1 -1
- package/dist/domain/map.js +57 -0
- package/dist/domain/map.js.map +1 -1
- package/dist/domain/models.d.ts +33 -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 +2 -0
- package/dist/generated/client.generated.d.ts.map +1 -1
- package/dist/generated/client.generated.js +2 -0
- package/dist/generated/client.generated.js.map +1 -1
- package/dist/generated/opcodes.generated.d.ts +2 -0
- package/dist/generated/opcodes.generated.d.ts.map +1 -1
- package/dist/generated/opcodes.generated.js +2 -0
- package/dist/generated/opcodes.generated.js.map +1 -1
- package/dist/generated/operations.generated.d.ts +24 -3
- package/dist/generated/operations.generated.d.ts.map +1 -1
- package/dist/generated/operations.generated.js +4 -2
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/mcp/confirm.d.ts +6 -2
- package/dist/mcp/confirm.d.ts.map +1 -1
- package/dist/mcp/confirm.js +2 -2
- package/dist/mcp/confirm.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 +187 -12
- package/dist/mcp/tools.js.map +1 -1
- package/dist/moderation/check.d.ts +105 -0
- package/dist/moderation/check.d.ts.map +1 -0
- package/dist/moderation/check.js +313 -0
- package/dist/moderation/check.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/rendering/messages.d.ts +6 -1
- package/dist/rendering/messages.d.ts.map +1 -1
- package/dist/rendering/messages.js +24 -3
- package/dist/rendering/messages.js.map +1 -1
- package/dist/review.d.ts +10 -4
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +35 -53
- 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/server/server-connection.d.ts +8 -1
- package/dist/server/server-connection.d.ts.map +1 -1
- package/dist/server/server-connection.js +27 -1
- package/dist/server/server-connection.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +2 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/start.d.ts +1 -1
- package/dist/server/start.d.ts.map +1 -1
- package/dist/server/start.js +5 -3
- package/dist/server/start.js.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 +4 -2
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/operations/chats.d.ts.map +1 -1
- package/dist/spec/operations/chats.js +3 -1
- package/dist/spec/operations/chats.js.map +1 -1
- package/dist/spec/operations/messages.d.ts +13 -3
- package/dist/spec/operations/messages.d.ts.map +1 -1
- package/dist/spec/operations/messages.js +37 -6
- 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 +6 -2
- package/skills/max-cli/SKILL.md +17 -5
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, toGroupMember, toMessage, toProfile, toReactions, toSession, } from "./domain/map.js";
|
|
2
|
+
import { namesFrom, POLL_CLOSED, pollSettings, SETTING_FLAGS, toChat, toContact, toFolder, toGroupCard, toGroupMember, toMessage, toPoll, 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
|
|
@@ -209,41 +210,18 @@ export class MaxClient {
|
|
|
209
210
|
* come from the local copy.
|
|
210
211
|
*/
|
|
211
212
|
adminIds: async (chatId) => {
|
|
212
|
-
await this.#
|
|
213
|
-
|
|
214
|
-
if (!raw || (raw.owner === undefined && raw.admins === undefined && raw.adminParticipants === undefined)) {
|
|
215
|
-
return undefined;
|
|
216
|
-
}
|
|
217
|
-
const ids = [
|
|
218
|
-
asId(raw.owner),
|
|
219
|
-
...(Array.isArray(raw.admins) ? raw.admins.map(asId) : []),
|
|
220
|
-
...Object.keys(record(raw.adminParticipants) ?? {}),
|
|
221
|
-
];
|
|
222
|
-
return [...new Set(ids.filter((id) => id !== undefined))];
|
|
213
|
+
const roles = await this.#roles(chatId);
|
|
214
|
+
return roles && [...new Set([...(roles.owner ? [roles.owner] : []), ...roles.admins])];
|
|
223
215
|
},
|
|
224
216
|
/**
|
|
225
217
|
* Who joined, left, was added or removed since a point: the service messages in the chat's
|
|
226
218
|
* history, read forward from `since` without reactions. At most `EVENTS_READ` messages, the oldest.
|
|
227
219
|
*/
|
|
228
|
-
events: async (reference, { since }) => {
|
|
220
|
+
events: async (reference, { since = Date.now() - EVENTS_DAYS * 86_400_000 } = {}) => {
|
|
229
221
|
if (this.#offline)
|
|
230
222
|
throw new CliError("validation_error", "`--offline` has no history to read events from");
|
|
231
223
|
const chatId = await this.chats.resolve(reference);
|
|
232
|
-
const read =
|
|
233
|
-
let from = since;
|
|
234
|
-
let more = false;
|
|
235
|
-
while (true) {
|
|
236
|
-
const page = (await this.#history(chatId, { from, backward: 0, forward: REVIEW_PAGE + 1 }, { reactions: false })).filter((message) => Date.parse(message.timestamp) > from);
|
|
237
|
-
read.push(...page);
|
|
238
|
-
const last = page.at(-1);
|
|
239
|
-
if (page.length < REVIEW_PAGE || !last)
|
|
240
|
-
break;
|
|
241
|
-
if (read.length >= EVENTS_READ) {
|
|
242
|
-
more = true;
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
from = Date.parse(last.timestamp);
|
|
246
|
-
}
|
|
224
|
+
const { messages: read, more } = await this.chats.since(chatId, since);
|
|
247
225
|
const found = read.flatMap((message) => message.attachments
|
|
248
226
|
.filter((attachment) => attachment.kind === "control" && attachment.event !== undefined)
|
|
249
227
|
.map((attachment) => ({ message, attachment })));
|
|
@@ -270,6 +248,24 @@ export class MaxClient {
|
|
|
270
248
|
})),
|
|
271
249
|
};
|
|
272
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
|
+
},
|
|
273
269
|
/** What a link leads to, without joining it. */
|
|
274
270
|
inspect: async (link) => {
|
|
275
271
|
if (this.#offline)
|
|
@@ -367,7 +363,14 @@ export class MaxClient {
|
|
|
367
363
|
}
|
|
368
364
|
marker = next;
|
|
369
365
|
}
|
|
370
|
-
|
|
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
|
+
};
|
|
371
374
|
},
|
|
372
375
|
/** No history unless asked (`NEED-272`): what was said before somebody joined is not theirs by default. */
|
|
373
376
|
add: (reference, people, { history = false } = {}) => this.#updateMembers(reference, people, "members.add", { operation: "add", showHistory: history }),
|
|
@@ -382,13 +385,14 @@ export class MaxClient {
|
|
|
382
385
|
remove: (reference, person) => this.#updateMembers(reference, [person], "admins.remove", { operation: "remove", type: "ADMIN" }),
|
|
383
386
|
},
|
|
384
387
|
requests: {
|
|
388
|
+
/** Each with the age of their account, read like a member's — the answer's `{contact}` is the same shape. */
|
|
385
389
|
list: async (reference) => {
|
|
386
390
|
if (this.#offline)
|
|
387
391
|
throw new CliError("validation_error", "`--offline` reads what was recorded; join requests never are");
|
|
388
392
|
const chatId = await this.chats.resolve(reference);
|
|
389
393
|
await this.#connectOnce();
|
|
390
394
|
const answer = await this.#wire.chats.members({ chatId, type: "JOIN_REQUEST", count: JOIN_REQUESTS });
|
|
391
|
-
return asArray(answer.members).map(
|
|
395
|
+
return asArray(answer.members).map(toGroupMember);
|
|
392
396
|
},
|
|
393
397
|
accept: (reference, people) => this.#updateMembers(reference, people, "requests.accept", {
|
|
394
398
|
operation: "add",
|
|
@@ -791,6 +795,10 @@ export class MaxClient {
|
|
|
791
795
|
send: async (chatId, text, options = {}) => {
|
|
792
796
|
if (this.#offline)
|
|
793
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
|
+
}
|
|
794
802
|
if (options.at !== undefined && options.notify === false) {
|
|
795
803
|
// The web client always sends a scheduled message with `notify: true`; §34.
|
|
796
804
|
throw new CliError("validation_error", "a scheduled message cannot be silent — MAX's own client never sends one");
|
|
@@ -802,11 +810,16 @@ export class MaxClient {
|
|
|
802
810
|
const files = await Promise.all((options.files ?? []).map(async (path) => ({
|
|
803
811
|
path,
|
|
804
812
|
bytes: await readUpload(path, { anyFile: options.anyFile === true }),
|
|
805
|
-
|
|
813
|
+
kind: isImage(path) ? "photo" : isVideo(path) && options.asFile !== true ? "video" : "file",
|
|
806
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
|
+
}
|
|
807
819
|
// Measured 2026-09-24: photos share a message, but a file with anything beside it is refused `proto.payload`.
|
|
808
|
-
|
|
809
|
-
|
|
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");
|
|
810
823
|
}
|
|
811
824
|
// Before connecting: a refused send never opens a socket when the chat was given as an id.
|
|
812
825
|
try {
|
|
@@ -822,10 +835,7 @@ export class MaxClient {
|
|
|
822
835
|
throw error;
|
|
823
836
|
}
|
|
824
837
|
const cid = options.cid ?? this.#nextCid();
|
|
825
|
-
const attachments = files.map(({ bytes,
|
|
826
|
-
kind: photo ? "photo" : "file",
|
|
827
|
-
bytes: bytes.length,
|
|
828
|
-
}));
|
|
838
|
+
const attachments = files.map(({ bytes, kind }) => ({ kind, bytes: bytes.length }));
|
|
829
839
|
const summary = {
|
|
830
840
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
831
841
|
...(options.at === undefined ? {} : { scheduledFor: new Date(options.at).toISOString() }),
|
|
@@ -992,6 +1002,160 @@ export class MaxClient {
|
|
|
992
1002
|
}
|
|
993
1003
|
},
|
|
994
1004
|
};
|
|
1005
|
+
/**
|
|
1006
|
+
* **Polls: vote, take a vote back, close one's own, create one.** Every one of them is a write
|
|
1007
|
+
* somebody else can see, so it passes the send guard and the journal under the kind of what it is:
|
|
1008
|
+
* a vote is a `reaction`, closing is an `edit` of the owner's message, a new poll is a `message`.
|
|
1009
|
+
* A vote is never retried — nothing measured makes a blind repeat safe.
|
|
1010
|
+
*
|
|
1011
|
+
* The shapes were measured in Saved messages on 2026-09-27 (`pnpm probe:polls`, `FIND-247`). The
|
|
1012
|
+
* checks before sending are the web client's, so what it would refuse never reaches MAX.
|
|
1013
|
+
*/
|
|
1014
|
+
polls = {
|
|
1015
|
+
vote: async (chatId, messageId, answerIds) => {
|
|
1016
|
+
if (this.#offline)
|
|
1017
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot vote");
|
|
1018
|
+
this.#guard({ chatId, kind: "reaction" }, messageId);
|
|
1019
|
+
try {
|
|
1020
|
+
await this.#connectOnce();
|
|
1021
|
+
const { attach, poll } = await this.#poll(chatId, messageId);
|
|
1022
|
+
const voted = poll.answers.some((answer) => answer.mine);
|
|
1023
|
+
if (answerIds.length === 0 && !voted) {
|
|
1024
|
+
throw new CliError("validation_error", `you have not voted in the poll of message ${messageId}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (answerIds.length === 0 && !poll.revote) {
|
|
1027
|
+
throw new CliError("validation_error", `the poll of message ${messageId} does not let a vote be taken back`);
|
|
1028
|
+
}
|
|
1029
|
+
if (answerIds.length > 0 && voted && !poll.revote) {
|
|
1030
|
+
throw new CliError("validation_error", `you have voted in the poll of message ${messageId}, and it does not let a vote change`);
|
|
1031
|
+
}
|
|
1032
|
+
if (answerIds.length > 1 && !poll.multiple) {
|
|
1033
|
+
throw new CliError("validation_error", `the poll of message ${messageId} takes one answer, not ${answerIds.length}`);
|
|
1034
|
+
}
|
|
1035
|
+
const unknown = answerIds.find((id) => !poll.answers.some((answer) => answer.id === id));
|
|
1036
|
+
if (unknown !== undefined) {
|
|
1037
|
+
throw new CliError("validation_error", `the poll of message ${messageId} has no answer ${unknown} — its answers are ${poll.answers.map((answer) => answer.id).join(", ")}`);
|
|
1038
|
+
}
|
|
1039
|
+
const answer = await this.#wire.messages.pollVote({
|
|
1040
|
+
chatId,
|
|
1041
|
+
messageId,
|
|
1042
|
+
pollId: poll.id,
|
|
1043
|
+
answersIds: answerIds.map(Number),
|
|
1044
|
+
});
|
|
1045
|
+
this.#cache?.messages.invalidate(chatId);
|
|
1046
|
+
this.#sends?.record({ chatId, kind: "reaction", outcome: "sent", messageId });
|
|
1047
|
+
const state = record(answer.state);
|
|
1048
|
+
return { chatId, messageId, poll: (state && toPoll({ ...attach, state })) ?? poll };
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
this.#sends?.record({
|
|
1052
|
+
chatId,
|
|
1053
|
+
kind: "reaction",
|
|
1054
|
+
outcome: "failed",
|
|
1055
|
+
messageId,
|
|
1056
|
+
errorCode: asCliError(error).code,
|
|
1057
|
+
});
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
/** Closes the owner's own poll: the web client edits the message with the `closed` bit raised. */
|
|
1062
|
+
close: async (chatId, messageId) => {
|
|
1063
|
+
if (this.#offline)
|
|
1064
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot close a poll");
|
|
1065
|
+
this.#guard({ chatId, kind: "edit" }, messageId);
|
|
1066
|
+
try {
|
|
1067
|
+
await this.#connectOnce();
|
|
1068
|
+
const { attach, poll, outgoing } = await this.#poll(chatId, messageId);
|
|
1069
|
+
if (!outgoing)
|
|
1070
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is not yours to close`);
|
|
1071
|
+
// What the web client sends back: the answers' text only, no ids and no votes.
|
|
1072
|
+
const closed = {
|
|
1073
|
+
_type: "POLL",
|
|
1074
|
+
pollId: attach.pollId,
|
|
1075
|
+
title: attach.title,
|
|
1076
|
+
answers: asArray(attach.answers).map((each) => ({ text: each.text })),
|
|
1077
|
+
settings: (typeof attach.settings === "number" ? attach.settings : 0) | POLL_CLOSED,
|
|
1078
|
+
};
|
|
1079
|
+
const answer = await this.#wire.messages.edit({ chatId, messageId, attachments: [closed] });
|
|
1080
|
+
this.#cache?.messages.invalidate(chatId);
|
|
1081
|
+
this.#sends?.record({ chatId, kind: "edit", outcome: "sent", messageId });
|
|
1082
|
+
const after = asArray(record(answer.message)?.attaches).find((each) => each._type === "POLL");
|
|
1083
|
+
return { chatId, messageId, poll: (after && toPoll(after)) ?? { ...poll, closed: true } };
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
this.#sends?.record({ chatId, kind: "edit", outcome: "failed", messageId, errorCode: asCliError(error).code });
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
/**
|
|
1091
|
+
* A new message whose one attachment is the poll — so it goes through `#deliver`, with its `cid`
|
|
1092
|
+
* and its one retry, like any message.
|
|
1093
|
+
*/
|
|
1094
|
+
create: async (chatId, question, answers, options = {}) => {
|
|
1095
|
+
if (this.#offline)
|
|
1096
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot create a poll");
|
|
1097
|
+
if (question.trim() === "")
|
|
1098
|
+
throw new CliError("validation_error", "a poll needs a question");
|
|
1099
|
+
if (answers.length < 2 || answers.some((answer) => answer.trim() === "")) {
|
|
1100
|
+
throw new CliError("validation_error", "a poll needs at least two answers, none of them empty");
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
this.#sends?.check({ chatId, kind: "message", ...(options.cid === undefined ? {} : { cid: options.cid }) });
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
this.#sends?.record({ chatId, kind: "message", outcome: "refused", errorCode: asCliError(error).code });
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
const cid = options.cid ?? this.#nextCid();
|
|
1110
|
+
const attach = {
|
|
1111
|
+
_type: "POLL",
|
|
1112
|
+
title: question,
|
|
1113
|
+
answers: answers.map((text) => ({ text })),
|
|
1114
|
+
settings: pollSettings(options),
|
|
1115
|
+
};
|
|
1116
|
+
try {
|
|
1117
|
+
const sent = await this.#deliver(chatId, "", cid, {
|
|
1118
|
+
...options,
|
|
1119
|
+
attaches: [attach],
|
|
1120
|
+
repeat: `max polls create ${chatId} …`,
|
|
1121
|
+
});
|
|
1122
|
+
this.#sends?.record({
|
|
1123
|
+
chatId,
|
|
1124
|
+
kind: "message",
|
|
1125
|
+
outcome: "sent",
|
|
1126
|
+
messageId: sent.id,
|
|
1127
|
+
cid,
|
|
1128
|
+
length: question.length,
|
|
1129
|
+
});
|
|
1130
|
+
return sent;
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
const failure = asCliError(error);
|
|
1134
|
+
this.#sends?.record({
|
|
1135
|
+
chatId,
|
|
1136
|
+
kind: "message",
|
|
1137
|
+
outcome: failure.code === "outcome_unknown" ? "outcome_unknown" : "failed",
|
|
1138
|
+
cid,
|
|
1139
|
+
errorCode: failure.code,
|
|
1140
|
+
});
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
/** The poll of one message, whole as MAX sends it and read, or a refusal that says why there is none. */
|
|
1146
|
+
async #poll(chatId, messageId) {
|
|
1147
|
+
const raw = await this.#rawMessage(chatId, messageId);
|
|
1148
|
+
const attach = asArray(raw.attaches).find((each) => each._type === "POLL");
|
|
1149
|
+
if (!attach)
|
|
1150
|
+
throw new CliError("validation_error", `message ${messageId} carries no poll`);
|
|
1151
|
+
const poll = toPoll(attach);
|
|
1152
|
+
if (!poll)
|
|
1153
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is a newer kind this version cannot read`);
|
|
1154
|
+
if (poll.closed)
|
|
1155
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is closed`);
|
|
1156
|
+
const lookup = { names: namesFrom(this.#session().contacts), ...viewer(this.#store) };
|
|
1157
|
+
return { attach, poll, outgoing: toMessage(raw, chatId, lookup).outgoing === true };
|
|
1158
|
+
}
|
|
995
1159
|
folders = {
|
|
996
1160
|
list: async () => (await this.#folders()).map(toFolder),
|
|
997
1161
|
create: (title, chats = []) => this.#change("folder-create", async () => {
|
|
@@ -1208,7 +1372,7 @@ export class MaxClient {
|
|
|
1208
1372
|
async #deliver(chatId, text, cid, options) {
|
|
1209
1373
|
await this.#connectOnce();
|
|
1210
1374
|
const session = this.#session();
|
|
1211
|
-
const attaches = [];
|
|
1375
|
+
const attaches = [...(options.attaches ?? [])];
|
|
1212
1376
|
for (const file of options.files ?? [])
|
|
1213
1377
|
attaches.push(await this.#upload(file));
|
|
1214
1378
|
const { text: plain, markup } = options.markdown ? parseMarkdown(text) : { text, markup: [] };
|
|
@@ -1703,6 +1867,20 @@ export class MaxClient {
|
|
|
1703
1867
|
forwardedFrom: message.forwardedFrom && name(message.forwardedFrom),
|
|
1704
1868
|
}));
|
|
1705
1869
|
}
|
|
1870
|
+
/** The chat's owner and admins as the login carried them; `undefined` when it did not. */
|
|
1871
|
+
async #roles(chatId) {
|
|
1872
|
+
await this.#connectOnce();
|
|
1873
|
+
const raw = asArray(this.#session().chats).find((chat) => asId(chat.id) === chatId);
|
|
1874
|
+
if (!raw || (raw.owner === undefined && raw.admins === undefined && raw.adminParticipants === undefined)) {
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
const admins = [
|
|
1878
|
+
...(Array.isArray(raw.admins) ? raw.admins.map(asId) : []),
|
|
1879
|
+
...Object.keys(record(raw.adminParticipants) ?? {}),
|
|
1880
|
+
].filter((id) => id !== undefined);
|
|
1881
|
+
const owner = asId(raw.owner);
|
|
1882
|
+
return { ...(owner ? { owner } : {}), admins: new Set(admins.filter((id) => id !== owner)) };
|
|
1883
|
+
}
|
|
1706
1884
|
/** Names we hold first, the rest from `CONTACT_INFO`, kept for next time. A refusal costs the names only. */
|
|
1707
1885
|
async #namesOf(ids) {
|
|
1708
1886
|
const names = this.#cache?.people.names(ids) ?? new Map();
|
|
@@ -1832,14 +2010,25 @@ export class MaxClient {
|
|
|
1832
2010
|
* Uploads one file and answers what the message attaches (measured 2026-09-24). An upload is never
|
|
1833
2011
|
* retried: a failure here happens before `MSG_SEND`, so nothing was sent.
|
|
1834
2012
|
*/
|
|
1835
|
-
async #upload({ path, bytes,
|
|
2013
|
+
async #upload({ path, bytes, kind, voice }) {
|
|
1836
2014
|
const request = { count: 1, type: 0, uploaderType: 0, profile: false };
|
|
1837
|
-
if (photo) {
|
|
2015
|
+
if (kind === "photo") {
|
|
1838
2016
|
const { url } = await this.#wire.uploads.photo(request);
|
|
1839
2017
|
if (typeof url !== "string")
|
|
1840
2018
|
throw new CliError("provider_error", "MAX gave no address to upload the photo to");
|
|
1841
2019
|
return { _type: "PHOTO", photoToken: await uploadPhoto(url, path, bytes) };
|
|
1842
2020
|
}
|
|
2021
|
+
if (kind === "video" || kind === "voice") {
|
|
2022
|
+
const slot = voice ? { ...request, type: 2, uploaderType: 1 } : request;
|
|
2023
|
+
const info = record(asArray((await this.#wire.uploads.video(slot)).info)[0]) ?? {};
|
|
2024
|
+
if (typeof info.url !== "string" || info.videoId === undefined) {
|
|
2025
|
+
throw new CliError("provider_error", `MAX gave no address to upload the ${kind} to`);
|
|
2026
|
+
}
|
|
2027
|
+
await uploadMedia(voice ? "voice message" : "video", info.url, path, bytes);
|
|
2028
|
+
return voice
|
|
2029
|
+
? { _type: "AUDIO", audioId: info.videoId, duration: voice.durationMs, wave: voice.wave, token: info.token }
|
|
2030
|
+
: { _type: "VIDEO", videoId: info.videoId, token: info.token, videoType: 0 };
|
|
2031
|
+
}
|
|
1843
2032
|
const info = record(asArray((await this.#wire.uploads.file(request)).info)[0]) ?? {};
|
|
1844
2033
|
if (typeof info.url !== "string" || info.fileId === undefined) {
|
|
1845
2034
|
throw new CliError("provider_error", "MAX gave no address to upload the file to");
|
|
@@ -2161,6 +2350,8 @@ const REVIEW_CHATS = 50;
|
|
|
2161
2350
|
const REVIEW_PAGE = 100;
|
|
2162
2351
|
const REVIEW_PER_CHAT = 500;
|
|
2163
2352
|
const EVENTS_READ = 2000;
|
|
2353
|
+
/** How far back `chats events` looks without `since`. */
|
|
2354
|
+
export const EVENTS_DAYS = 7;
|
|
2164
2355
|
export const FIRST_TAB_SYNC = { folders: 0, calls: 0, assets: {} };
|
|
2165
2356
|
const numberOr = (value, fallback) => (typeof value === "number" ? value : fallback);
|
|
2166
2357
|
const eventTime = (chat) => {
|