@leemour/max-cli 0.15.0 → 0.17.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 +56 -37
- package/dist/bot/client.d.ts +6 -0
- package/dist/bot/client.d.ts.map +1 -1
- package/dist/bot/client.js +17 -3
- 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 +2 -2
- package/dist/bot/keep.d.ts.map +1 -1
- package/dist/bot/keep.js +3 -2
- package/dist/bot/keep.js.map +1 -1
- 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/transport.d.ts +4 -0
- package/dist/bot/transport.d.ts.map +1 -1
- package/dist/bot/transport.js +43 -3
- package/dist/bot/transport.js.map +1 -1
- 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 +213 -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 +43 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +207 -31
- package/dist/client.js.map +1 -1
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +7 -5
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/backup.js +1 -1
- package/dist/commands/backup.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.map +1 -1
- package/dist/commands/bot-comments.js +2 -1
- package/dist/commands/bot-comments.js.map +1 -1
- package/dist/commands/bot-context.d.ts +7 -0
- package/dist/commands/bot-context.d.ts.map +1 -1
- package/dist/commands/bot-context.js +32 -2
- 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.map +1 -1
- package/dist/commands/bot-members.js +7 -4
- package/dist/commands/bot-members.js.map +1 -1
- 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 +25 -14
- package/dist/commands/bot-reads.js.map +1 -1
- package/dist/commands/bot-sends.d.ts.map +1 -1
- package/dist/commands/bot-sends.js +6 -5
- package/dist/commands/bot-sends.js.map +1 -1
- package/dist/commands/bot-setup.d.ts.map +1 -1
- package/dist/commands/bot-setup.js +5 -4
- package/dist/commands/bot-setup.js.map +1 -1
- package/dist/commands/bot-updates.d.ts.map +1 -1
- package/dist/commands/bot-updates.js +16 -2
- package/dist/commands/bot-updates.js.map +1 -1
- package/dist/commands/bot.d.ts.map +1 -1
- package/dist/commands/bot.js +13 -4
- package/dist/commands/bot.js.map +1 -1
- package/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +15 -30
- package/dist/commands/chats.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +7 -6
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/complete.d.ts.map +1 -1
- package/dist/commands/complete.js +27 -3
- package/dist/commands/complete.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +29 -27
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +21 -0
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +75 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/export.js +1 -1
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/folders.d.ts.map +1 -1
- package/dist/commands/folders.js +3 -2
- package/dist/commands/folders.js.map +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +9 -11
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +2 -1
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/paging.d.ts +17 -1
- package/dist/commands/paging.d.ts.map +1 -1
- package/dist/commands/paging.js +22 -2
- 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/recipients.d.ts.map +1 -1
- package/dist/commands/recipients.js +3 -2
- package/dist/commands/recipients.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/runs.js +4 -3
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/sends.d.ts.map +1 -1
- package/dist/commands/sends.js +3 -2
- package/dist/commands/sends.js.map +1 -1
- package/dist/config.d.ts +78 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +143 -72
- package/dist/config.js.map +1 -1
- package/dist/diagnose.d.ts +47 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +56 -5
- package/dist/diagnose.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 +29 -0
- package/dist/domain/models.d.ts.map +1 -1
- package/dist/domain/models.js.map +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +2 -1
- package/dist/export.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 +25 -10
- 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/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/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +83 -17
- package/dist/mcp/tools.js.map +1 -1
- package/dist/moderation/check.d.ts +15 -9
- package/dist/moderation/check.d.ts.map +1 -1
- package/dist/moderation/check.js +14 -45
- package/dist/moderation/check.js.map +1 -1
- package/dist/moderation/rules.d.ts +57 -7
- package/dist/moderation/rules.d.ts.map +1 -1
- package/dist/moderation/rules.js +13 -11
- package/dist/moderation/rules.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +11 -3
- 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/runs/events.d.ts +5 -2
- package/dist/runs/events.d.ts.map +1 -1
- package/dist/runs/events.js +4 -1
- package/dist/runs/events.js.map +1 -1
- package/dist/runs/recording.d.ts +11 -0
- package/dist/runs/recording.d.ts.map +1 -1
- package/dist/runs/recording.js +28 -12
- package/dist/runs/recording.js.map +1 -1
- package/dist/sends/guard.d.ts +2 -2
- package/dist/sends/guard.d.ts.map +1 -1
- package/dist/sends/guard.js +2 -1
- package/dist/sends/guard.js.map +1 -1
- package/dist/sends/journal.d.ts +2 -2
- package/dist/sends/journal.d.ts.map +1 -1
- package/dist/sends/journal.js.map +1 -1
- package/dist/sends/permissions.d.ts.map +1 -1
- package/dist/sends/permissions.js +3 -0
- package/dist/sends/permissions.js.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 +7 -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 +5 -1
- 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/account.d.ts +3 -0
- package/dist/spec/operations/account.d.ts.map +1 -1
- package/dist/spec/operations/account.js +5 -1
- package/dist/spec/operations/account.js.map +1 -1
- package/dist/spec/operations/chats.d.ts +4 -3
- package/dist/spec/operations/chats.d.ts.map +1 -1
- package/dist/spec/operations/chats.js +12 -7
- package/dist/spec/operations/chats.js.map +1 -1
- package/dist/spec/operations/contacts.d.ts +8 -3
- package/dist/spec/operations/contacts.d.ts.map +1 -1
- package/dist/spec/operations/contacts.js +26 -5
- package/dist/spec/operations/contacts.js.map +1 -1
- package/dist/spec/operations/folders.d.ts.map +1 -1
- package/dist/spec/operations/folders.js +1 -0
- package/dist/spec/operations/folders.js.map +1 -1
- package/dist/spec/operations/messages.d.ts +14 -4
- package/dist/spec/operations/messages.d.ts.map +1 -1
- package/dist/spec/operations/messages.js +30 -3
- package/dist/spec/operations/messages.js.map +1 -1
- package/dist/spec/operations/uploads.d.ts +1 -1
- package/dist/spec/operations/uploads.d.ts.map +1 -1
- package/dist/spec/operations/uploads.js +2 -1
- package/dist/spec/operations/uploads.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/max-cli/SKILL.md +19 -9
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@leemour/cli-core";
|
|
2
|
-
import {
|
|
2
|
+
import { delayMs } from "./config.js";
|
|
3
|
+
import { namesFrom, POLL_CLOSED, pollSettings, SETTING_FLAGS, toChat, toContact, toFolder, toGroupCard, toGroupMember, toMessage, toPoll, toProfile, toReactions, toSession, } from "./domain/map.js";
|
|
3
4
|
import { heldWindows } from "./export.js";
|
|
4
5
|
import { wireClient } from "./generated/client.generated.js";
|
|
5
6
|
import { parseMarkdown } from "./markdown.js";
|
|
@@ -71,10 +72,12 @@ export class MaxClient {
|
|
|
71
72
|
if (!firstName)
|
|
72
73
|
throw new CliError("validation_error", "MAX needs a first name, and the profile has none — pass --first-name");
|
|
73
74
|
const lastName = change.lastName ?? current.lastName;
|
|
75
|
+
const photoToken = change.photo === undefined ? undefined : await this.#profilePhoto(change.photo);
|
|
74
76
|
const answer = await this.#wire.account.update({
|
|
75
77
|
firstName,
|
|
76
78
|
...(lastName === undefined ? {} : { lastName }),
|
|
77
79
|
...(change.description === undefined ? {} : { description: change.description }),
|
|
80
|
+
...(photoToken === undefined ? {} : { photoToken, avatarType: "USER_AVATAR" }),
|
|
78
81
|
});
|
|
79
82
|
return toProfile(record(answer.profile) ?? {});
|
|
80
83
|
}),
|
|
@@ -272,12 +275,13 @@ export class MaxClient {
|
|
|
272
275
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot read a link");
|
|
273
276
|
const wire = wireLink(link);
|
|
274
277
|
await this.#connectOnce();
|
|
275
|
-
|
|
278
|
+
const answer = await this.#wire.chats.linkInfo({ link: wire }).catch(deadLink(link));
|
|
279
|
+
return toGroupCard(record(answer.chat) ?? {});
|
|
276
280
|
},
|
|
277
281
|
join: (link) => {
|
|
278
282
|
const wire = wireLink(link);
|
|
279
283
|
return this.#changeChat(null, "join", async () => {
|
|
280
|
-
const chat = toGroupCard(record((await this.#wire.chats.join({ link: wire })).chat) ?? {});
|
|
284
|
+
const chat = toGroupCard(record((await this.#wire.chats.join({ link: wire }).catch(deadLink(link))).chat) ?? {});
|
|
281
285
|
return { chatId: chat.id, result: chat };
|
|
282
286
|
});
|
|
283
287
|
},
|
|
@@ -320,13 +324,13 @@ export class MaxClient {
|
|
|
320
324
|
* Not retried, unlike a message: a second attempt with a new `cid` is a second group, and
|
|
321
325
|
* whether MAX deduplicates a creation by `cid` is not measured.
|
|
322
326
|
*/
|
|
323
|
-
create: async (title, people = []) => {
|
|
327
|
+
create: async (title, people = [], { channel = false } = {}) => {
|
|
324
328
|
const userIds = await this.#personIds(people);
|
|
325
329
|
return this.#changeChat(null, "create", async () => {
|
|
326
330
|
const answer = await this.#wire.messages.send({
|
|
327
331
|
message: {
|
|
328
332
|
cid: this.#nextCid(),
|
|
329
|
-
attaches: [{ _type: "CONTROL", event: "new", chatType: "CHAT", title, userIds }],
|
|
333
|
+
attaches: [{ _type: "CONTROL", event: "new", chatType: channel ? "CHANNEL" : "CHAT", title, userIds }],
|
|
330
334
|
},
|
|
331
335
|
notify: true,
|
|
332
336
|
});
|
|
@@ -384,23 +388,6 @@ export class MaxClient {
|
|
|
384
388
|
}),
|
|
385
389
|
remove: (reference, person) => this.#updateMembers(reference, [person], "admins.remove", { operation: "remove", type: "ADMIN" }),
|
|
386
390
|
},
|
|
387
|
-
requests: {
|
|
388
|
-
/** Each with the age of their account, read like a member's — the answer's `{contact}` is the same shape. */
|
|
389
|
-
list: async (reference) => {
|
|
390
|
-
if (this.#offline)
|
|
391
|
-
throw new CliError("validation_error", "`--offline` reads what was recorded; join requests never are");
|
|
392
|
-
const chatId = await this.chats.resolve(reference);
|
|
393
|
-
await this.#connectOnce();
|
|
394
|
-
const answer = await this.#wire.chats.members({ chatId, type: "JOIN_REQUEST", count: JOIN_REQUESTS });
|
|
395
|
-
return asArray(answer.members).map(toGroupMember);
|
|
396
|
-
},
|
|
397
|
-
accept: (reference, people) => this.#updateMembers(reference, people, "requests.accept", {
|
|
398
|
-
operation: "add",
|
|
399
|
-
type: "JOIN_REQUEST",
|
|
400
|
-
showHistory: true,
|
|
401
|
-
}),
|
|
402
|
-
decline: (reference, people) => this.#updateMembers(reference, people, "requests.decline", { operation: "remove", type: "JOIN_REQUEST" }),
|
|
403
|
-
},
|
|
404
391
|
update: async (reference, { title, description }) => {
|
|
405
392
|
if (title === undefined && description === undefined) {
|
|
406
393
|
throw new CliError("validation_error", "nothing to change — give --title, --description or both");
|
|
@@ -501,8 +488,13 @@ export class MaxClient {
|
|
|
501
488
|
throw new CliError("not_found", "MAX has nobody under that number");
|
|
502
489
|
return this.#remember(toContact(contact));
|
|
503
490
|
},
|
|
504
|
-
add: (reference) => this.#contactAction("contact-add", "ADD", reference),
|
|
505
|
-
remove: (reference) => this.#contactAction("contact-remove", "REMOVE", reference),
|
|
491
|
+
add: (reference) => this.#contactAction("contact-add", { action: "ADD" }, reference),
|
|
492
|
+
remove: (reference) => this.#contactAction("contact-remove", { action: "REMOVE" }, reference),
|
|
493
|
+
/** They can no longer write to you. Works for somebody who is not a contact too (measured). */
|
|
494
|
+
block: (reference) => this.#contactAction("contact-block", { action: "BLOCK" }, reference),
|
|
495
|
+
unblock: (reference) => this.#contactAction("contact-unblock", { action: "UNBLOCK" }, reference),
|
|
496
|
+
/** A name of your own for them, kept beside the one they chose; they do not see it. */
|
|
497
|
+
rename: (reference, firstName, lastName) => this.#contactAction("contact-rename", { action: "UPDATE", firstName, ...(lastName === undefined ? {} : { lastName }) }, reference),
|
|
506
498
|
/** Uploads phone numbers to MAX — other people's — under the names they were saved with. */
|
|
507
499
|
import: (entries) => this.#change("contact-import", async () => {
|
|
508
500
|
if (entries.length === 0)
|
|
@@ -769,9 +761,14 @@ export class MaxClient {
|
|
|
769
761
|
*/
|
|
770
762
|
moment: (reference, flag = "--before") => {
|
|
771
763
|
const wanted = reference.trim();
|
|
772
|
-
const
|
|
764
|
+
const ago = delayMs(wanted);
|
|
765
|
+
const time = /^\d+$/.test(wanted)
|
|
766
|
+
? timeOfMessageId(wanted)
|
|
767
|
+
: ago === undefined
|
|
768
|
+
? Date.parse(wanted)
|
|
769
|
+
: Date.now() - ago;
|
|
773
770
|
if (time === undefined || Number.isNaN(time)) {
|
|
774
|
-
throw new CliError("validation_error", `${flag} takes a message id
|
|
771
|
+
throw new CliError("validation_error", `${flag} takes a message id, an ISO 8601 time or 30m, 2h, 1d ago — not "${wanted}"`);
|
|
775
772
|
}
|
|
776
773
|
return time;
|
|
777
774
|
},
|
|
@@ -1002,6 +999,160 @@ export class MaxClient {
|
|
|
1002
999
|
}
|
|
1003
1000
|
},
|
|
1004
1001
|
};
|
|
1002
|
+
/**
|
|
1003
|
+
* **Polls: vote, take a vote back, close one's own, create one.** Every one of them is a write
|
|
1004
|
+
* somebody else can see, so it passes the send guard and the journal under the kind of what it is:
|
|
1005
|
+
* a vote is a `reaction`, closing is an `edit` of the owner's message, a new poll is a `message`.
|
|
1006
|
+
* A vote is never retried — nothing measured makes a blind repeat safe.
|
|
1007
|
+
*
|
|
1008
|
+
* The shapes were measured in Saved messages on 2026-09-27 (`pnpm probe:polls`, `FIND-247`). The
|
|
1009
|
+
* checks before sending are the web client's, so what it would refuse never reaches MAX.
|
|
1010
|
+
*/
|
|
1011
|
+
polls = {
|
|
1012
|
+
vote: async (chatId, messageId, answerIds) => {
|
|
1013
|
+
if (this.#offline)
|
|
1014
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot vote");
|
|
1015
|
+
this.#guard({ chatId, kind: "reaction" }, messageId);
|
|
1016
|
+
try {
|
|
1017
|
+
await this.#connectOnce();
|
|
1018
|
+
const { attach, poll } = await this.#poll(chatId, messageId);
|
|
1019
|
+
const voted = poll.answers.some((answer) => answer.mine);
|
|
1020
|
+
if (answerIds.length === 0 && !voted) {
|
|
1021
|
+
throw new CliError("validation_error", `you have not voted in the poll of message ${messageId}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (answerIds.length === 0 && !poll.revote) {
|
|
1024
|
+
throw new CliError("validation_error", `the poll of message ${messageId} does not let a vote be taken back`);
|
|
1025
|
+
}
|
|
1026
|
+
if (answerIds.length > 0 && voted && !poll.revote) {
|
|
1027
|
+
throw new CliError("validation_error", `you have voted in the poll of message ${messageId}, and it does not let a vote change`);
|
|
1028
|
+
}
|
|
1029
|
+
if (answerIds.length > 1 && !poll.multiple) {
|
|
1030
|
+
throw new CliError("validation_error", `the poll of message ${messageId} takes one answer, not ${answerIds.length}`);
|
|
1031
|
+
}
|
|
1032
|
+
const unknown = answerIds.find((id) => !poll.answers.some((answer) => answer.id === id));
|
|
1033
|
+
if (unknown !== undefined) {
|
|
1034
|
+
throw new CliError("validation_error", `the poll of message ${messageId} has no answer ${unknown} — its answers are ${poll.answers.map((answer) => answer.id).join(", ")}`);
|
|
1035
|
+
}
|
|
1036
|
+
const answer = await this.#wire.messages.pollVote({
|
|
1037
|
+
chatId,
|
|
1038
|
+
messageId,
|
|
1039
|
+
pollId: poll.id,
|
|
1040
|
+
answersIds: answerIds.map(Number),
|
|
1041
|
+
});
|
|
1042
|
+
this.#cache?.messages.invalidate(chatId);
|
|
1043
|
+
this.#sends?.record({ chatId, kind: "reaction", outcome: "sent", messageId });
|
|
1044
|
+
const state = record(answer.state);
|
|
1045
|
+
return { chatId, messageId, poll: (state && toPoll({ ...attach, state })) ?? poll };
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
this.#sends?.record({
|
|
1049
|
+
chatId,
|
|
1050
|
+
kind: "reaction",
|
|
1051
|
+
outcome: "failed",
|
|
1052
|
+
messageId,
|
|
1053
|
+
errorCode: asCliError(error).code,
|
|
1054
|
+
});
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
/** Closes the owner's own poll: the web client edits the message with the `closed` bit raised. */
|
|
1059
|
+
close: async (chatId, messageId) => {
|
|
1060
|
+
if (this.#offline)
|
|
1061
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot close a poll");
|
|
1062
|
+
this.#guard({ chatId, kind: "edit" }, messageId);
|
|
1063
|
+
try {
|
|
1064
|
+
await this.#connectOnce();
|
|
1065
|
+
const { attach, poll, outgoing } = await this.#poll(chatId, messageId);
|
|
1066
|
+
if (!outgoing)
|
|
1067
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is not yours to close`);
|
|
1068
|
+
// What the web client sends back: the answers' text only, no ids and no votes.
|
|
1069
|
+
const closed = {
|
|
1070
|
+
_type: "POLL",
|
|
1071
|
+
pollId: attach.pollId,
|
|
1072
|
+
title: attach.title,
|
|
1073
|
+
answers: asArray(attach.answers).map((each) => ({ text: each.text })),
|
|
1074
|
+
settings: (typeof attach.settings === "number" ? attach.settings : 0) | POLL_CLOSED,
|
|
1075
|
+
};
|
|
1076
|
+
const answer = await this.#wire.messages.edit({ chatId, messageId, attachments: [closed] });
|
|
1077
|
+
this.#cache?.messages.invalidate(chatId);
|
|
1078
|
+
this.#sends?.record({ chatId, kind: "edit", outcome: "sent", messageId });
|
|
1079
|
+
const after = asArray(record(answer.message)?.attaches).find((each) => each._type === "POLL");
|
|
1080
|
+
return { chatId, messageId, poll: (after && toPoll(after)) ?? { ...poll, closed: true } };
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
this.#sends?.record({ chatId, kind: "edit", outcome: "failed", messageId, errorCode: asCliError(error).code });
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
/**
|
|
1088
|
+
* A new message whose one attachment is the poll — so it goes through `#deliver`, with its `cid`
|
|
1089
|
+
* and its one retry, like any message.
|
|
1090
|
+
*/
|
|
1091
|
+
create: async (chatId, question, answers, options = {}) => {
|
|
1092
|
+
if (this.#offline)
|
|
1093
|
+
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot create a poll");
|
|
1094
|
+
if (question.trim() === "")
|
|
1095
|
+
throw new CliError("validation_error", "a poll needs a question");
|
|
1096
|
+
if (answers.length < 2 || answers.some((answer) => answer.trim() === "")) {
|
|
1097
|
+
throw new CliError("validation_error", "a poll needs at least two answers, none of them empty");
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
this.#sends?.check({ chatId, kind: "message", ...(options.cid === undefined ? {} : { cid: options.cid }) });
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
this.#sends?.record({ chatId, kind: "message", outcome: "refused", errorCode: asCliError(error).code });
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
const cid = options.cid ?? this.#nextCid();
|
|
1107
|
+
const attach = {
|
|
1108
|
+
_type: "POLL",
|
|
1109
|
+
title: question,
|
|
1110
|
+
answers: answers.map((text) => ({ text })),
|
|
1111
|
+
settings: pollSettings(options),
|
|
1112
|
+
};
|
|
1113
|
+
try {
|
|
1114
|
+
const sent = await this.#deliver(chatId, "", cid, {
|
|
1115
|
+
...options,
|
|
1116
|
+
attaches: [attach],
|
|
1117
|
+
repeat: `max polls create ${chatId} …`,
|
|
1118
|
+
});
|
|
1119
|
+
this.#sends?.record({
|
|
1120
|
+
chatId,
|
|
1121
|
+
kind: "message",
|
|
1122
|
+
outcome: "sent",
|
|
1123
|
+
messageId: sent.id,
|
|
1124
|
+
cid,
|
|
1125
|
+
length: question.length,
|
|
1126
|
+
});
|
|
1127
|
+
return sent;
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
const failure = asCliError(error);
|
|
1131
|
+
this.#sends?.record({
|
|
1132
|
+
chatId,
|
|
1133
|
+
kind: "message",
|
|
1134
|
+
outcome: failure.code === "outcome_unknown" ? "outcome_unknown" : "failed",
|
|
1135
|
+
cid,
|
|
1136
|
+
errorCode: failure.code,
|
|
1137
|
+
});
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
};
|
|
1142
|
+
/** The poll of one message, whole as MAX sends it and read, or a refusal that says why there is none. */
|
|
1143
|
+
async #poll(chatId, messageId) {
|
|
1144
|
+
const raw = await this.#rawMessage(chatId, messageId);
|
|
1145
|
+
const attach = asArray(raw.attaches).find((each) => each._type === "POLL");
|
|
1146
|
+
if (!attach)
|
|
1147
|
+
throw new CliError("validation_error", `message ${messageId} carries no poll`);
|
|
1148
|
+
const poll = toPoll(attach);
|
|
1149
|
+
if (!poll)
|
|
1150
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is a newer kind this version cannot read`);
|
|
1151
|
+
if (poll.closed)
|
|
1152
|
+
throw new CliError("validation_error", `the poll of message ${messageId} is closed`);
|
|
1153
|
+
const lookup = { names: namesFrom(this.#session().contacts), ...viewer(this.#store) };
|
|
1154
|
+
return { attach, poll, outgoing: toMessage(raw, chatId, lookup).outgoing === true };
|
|
1155
|
+
}
|
|
1005
1156
|
folders = {
|
|
1006
1157
|
list: async () => (await this.#folders()).map(toFolder),
|
|
1007
1158
|
create: (title, chats = []) => this.#change("folder-create", async () => {
|
|
@@ -1218,7 +1369,7 @@ export class MaxClient {
|
|
|
1218
1369
|
async #deliver(chatId, text, cid, options) {
|
|
1219
1370
|
await this.#connectOnce();
|
|
1220
1371
|
const session = this.#session();
|
|
1221
|
-
const attaches = [];
|
|
1372
|
+
const attaches = [...(options.attaches ?? [])];
|
|
1222
1373
|
for (const file of options.files ?? [])
|
|
1223
1374
|
attaches.push(await this.#upload(file));
|
|
1224
1375
|
const { text: plain, markup } = options.markdown ? parseMarkdown(text) : { text, markup: [] };
|
|
@@ -1453,6 +1604,10 @@ export class MaxClient {
|
|
|
1453
1604
|
`the keyring is probably out of reach (cron, ssh: set XDG_RUNTIME_DIR); ` +
|
|
1454
1605
|
`\`max ${asFirstWord(profile)}doctor\` shows it. Log in again only if the token was removed`);
|
|
1455
1606
|
}
|
|
1607
|
+
if (this.#store.isBot()) {
|
|
1608
|
+
throw new CliError("authentication_error", `profile "${profile}" is a bot — its commands are \`max ${asFirstWord(profile)}bot …\`; ` +
|
|
1609
|
+
`\`max ${asFirstWord(profile)}session start\` would add a personal account to it`);
|
|
1610
|
+
}
|
|
1456
1611
|
// The fix has to carry the profile, or it logs the wrong one in: a name nobody has logged
|
|
1457
1612
|
// in under is the ordinary shape of this failure now that the first word is the profile.
|
|
1458
1613
|
throw new CliError("authentication_error", `no session for profile "${profile}" — run \`max ${asFirstWord(profile)}session start\``);
|
|
@@ -1856,6 +2011,15 @@ export class MaxClient {
|
|
|
1856
2011
|
* Uploads one file and answers what the message attaches (measured 2026-09-24). An upload is never
|
|
1857
2012
|
* retried: a failure here happens before `MSG_SEND`, so nothing was sent.
|
|
1858
2013
|
*/
|
|
2014
|
+
async #profilePhoto(path) {
|
|
2015
|
+
if (!isImage(path))
|
|
2016
|
+
throw new CliError("validation_error", `${path} is not an image MAX takes as a photo`);
|
|
2017
|
+
const bytes = await readUpload(path);
|
|
2018
|
+
const { url } = await this.#wire.uploads.photo({ count: 1, type: 0, uploaderType: 0, profile: true });
|
|
2019
|
+
if (typeof url !== "string")
|
|
2020
|
+
throw new CliError("provider_error", "MAX gave no address to upload the photo to");
|
|
2021
|
+
return uploadPhoto(url, path, bytes);
|
|
2022
|
+
}
|
|
1859
2023
|
async #upload({ path, bytes, kind, voice }) {
|
|
1860
2024
|
const request = { count: 1, type: 0, uploaderType: 0, profile: false };
|
|
1861
2025
|
if (kind === "photo") {
|
|
@@ -2081,7 +2245,7 @@ export class MaxClient {
|
|
|
2081
2245
|
throw new CliError("validation_error", "without a local store a person is named by id — `max contacts lookup` finds one");
|
|
2082
2246
|
}
|
|
2083
2247
|
const id = isId(reference) ? reference.trim() : pickPerson(reference, cache).id;
|
|
2084
|
-
const answer = await this.#wire.contacts.update({ contactId: id,
|
|
2248
|
+
const answer = await this.#wire.contacts.update({ contactId: id, ...wire });
|
|
2085
2249
|
const contact = record(answer.contact);
|
|
2086
2250
|
if (contact)
|
|
2087
2251
|
return this.#remember(toContact(contact));
|
|
@@ -2112,20 +2276,25 @@ export class MaxClient {
|
|
|
2112
2276
|
return this.#login;
|
|
2113
2277
|
}
|
|
2114
2278
|
}
|
|
2115
|
-
/**
|
|
2279
|
+
/**
|
|
2280
|
+
* PyMax `AdminPermission`; the sum is what MAX takes. `read` and `link` measured 2026-09-27 by
|
|
2281
|
+
* switching them in the app for a bot and reading its `permissions` back (`MAX-61`): «Read messages»
|
|
2282
|
+
* sets 1 and 32 together — the Bot API calls them `read_all_messages` and `write` — and «Update chat
|
|
2283
|
+
* link» 128. A bot without `read` sees no message of the group.
|
|
2284
|
+
*/
|
|
2116
2285
|
export const ADMIN_RIGHTS = {
|
|
2286
|
+
read: 1 | 32,
|
|
2117
2287
|
members: 2,
|
|
2118
2288
|
admins: 4,
|
|
2119
2289
|
info: 8,
|
|
2120
2290
|
pin: 16,
|
|
2291
|
+
link: 128,
|
|
2121
2292
|
post: 256,
|
|
2122
2293
|
edit: 512,
|
|
2123
2294
|
delete: 1024,
|
|
2124
2295
|
};
|
|
2125
|
-
/** PyMax asks for this many; paging join requests is not known. */
|
|
2126
2296
|
/** The most chats `max serve` holds from pushes before it logs in again instead. */
|
|
2127
2297
|
const LIVE_CHATS = 10_000;
|
|
2128
|
-
const JOIN_REQUESTS = 100;
|
|
2129
2298
|
/** PyMax's page, the one measured. */
|
|
2130
2299
|
const MEMBERS_PAGE = 50;
|
|
2131
2300
|
const MEMBERS_READ = 5000;
|
|
@@ -2316,6 +2485,13 @@ const asRefusal = (error) => {
|
|
|
2316
2485
|
}
|
|
2317
2486
|
return new CliError("provider_error", error.message, { operation: String(error.opcode) });
|
|
2318
2487
|
};
|
|
2488
|
+
/** A link that was reset, or never led anywhere, is `not.found` from MAX (measured 2026-09-28). */
|
|
2489
|
+
const deadLink = (link) => (error) => {
|
|
2490
|
+
const failure = asCliError(error);
|
|
2491
|
+
if (failure.details.maxError !== "not.found")
|
|
2492
|
+
throw failure;
|
|
2493
|
+
throw new CliError("not_found", `${link.trim()} leads nowhere — the link was reset, or never worked`);
|
|
2494
|
+
};
|
|
2319
2495
|
const LIMIT_WORDS = ["limit.violate", "rate_limit", "rate limit", "too many", "слишком много"];
|
|
2320
2496
|
/** Before any request: a login inside the pause is one more attempt MAX counts (`MAX-38`). */
|
|
2321
2497
|
export const refuseWhilePaused = (state) => {
|