@leemour/max-cli 0.10.0 → 0.12.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 +25 -4
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +10 -4
- package/dist/cache/index.js.map +1 -1
- package/dist/client.d.ts +35 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +178 -44
- package/dist/client.js.map +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +6 -2
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/cache.d.ts.map +1 -1
- package/dist/commands/cache.js +6 -5
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/chats.js +2 -2
- package/dist/commands/chats.js.map +1 -1
- package/dist/commands/complete.d.ts.map +1 -1
- package/dist/commands/complete.js +28 -22
- package/dist/commands/complete.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +5 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/context.d.ts +4 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +6 -14
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/doctor.js +51 -39
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +6 -4
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +29 -23
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/recipients.d.ts.map +1 -1
- package/dist/commands/recipients.js +25 -17
- package/dist/commands/recipients.js.map +1 -1
- package/dist/commands/sends.d.ts.map +1 -1
- package/dist/commands/sends.js +8 -6
- package/dist/commands/sends.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +43 -35
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +6 -10
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +73 -38
- package/dist/commands/watch.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -2
- package/dist/config.js.map +1 -1
- package/dist/diagnose.d.ts +4 -3
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +4 -4
- package/dist/diagnose.js.map +1 -1
- package/dist/domain/models.d.ts +20 -0
- package/dist/domain/models.d.ts.map +1 -1
- package/dist/domain/models.js.map +1 -1
- package/dist/download.d.ts +22 -3
- package/dist/download.d.ts.map +1 -1
- package/dist/download.js +133 -27
- package/dist/download.js.map +1 -1
- package/dist/export.d.ts +3 -0
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +13 -9
- package/dist/export.js.map +1 -1
- package/dist/generated/client.generated.d.ts +9 -0
- package/dist/generated/client.generated.d.ts.map +1 -1
- package/dist/generated/client.generated.js +9 -0
- package/dist/generated/client.generated.js.map +1 -1
- package/dist/generated/opcodes.generated.d.ts +3 -0
- package/dist/generated/opcodes.generated.d.ts.map +1 -1
- package/dist/generated/opcodes.generated.js +3 -0
- package/dist/generated/opcodes.generated.js.map +1 -1
- package/dist/generated/operations.generated.d.ts +20 -0
- package/dist/generated/operations.generated.d.ts.map +1 -1
- package/dist/generated/operations.generated.js +6 -0
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/mcp/confirm.d.ts +21 -9
- package/dist/mcp/confirm.d.ts.map +1 -1
- package/dist/mcp/confirm.js +55 -29
- package/dist/mcp/confirm.js.map +1 -1
- package/dist/mcp/instructions.js +3 -3
- package/dist/mcp/instructions.js.map +1 -1
- package/dist/mcp/prompts.d.ts +7 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +45 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +12 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +42 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +4 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +143 -15
- package/dist/mcp/tools.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +24 -2
- package/dist/output.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +51 -5
- package/dist/program.js.map +1 -1
- package/dist/protocol/connection.d.ts +2 -0
- package/dist/protocol/connection.d.ts.map +1 -1
- package/dist/protocol/connection.js +27 -5
- package/dist/protocol/connection.js.map +1 -1
- package/dist/protocol/frame.d.ts +2 -0
- package/dist/protocol/frame.d.ts.map +1 -1
- package/dist/protocol/frame.js +3 -1
- package/dist/protocol/frame.js.map +1 -1
- package/dist/protocol/lz4.js +8 -5
- package/dist/protocol/lz4.js.map +1 -1
- package/dist/rendering/messages.js +9 -10
- package/dist/rendering/messages.js.map +1 -1
- package/dist/report.d.ts +10 -6
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +45 -11
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +4 -3
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +9 -10
- package/dist/resolve.js.map +1 -1
- package/dist/runs/recording.d.ts +1 -0
- package/dist/runs/recording.d.ts.map +1 -1
- package/dist/runs/recording.js +13 -0
- package/dist/runs/recording.js.map +1 -1
- package/dist/sends/guard.d.ts +35 -4
- package/dist/sends/guard.d.ts.map +1 -1
- package/dist/sends/guard.js +128 -28
- package/dist/sends/guard.js.map +1 -1
- package/dist/sends/journal.d.ts +17 -3
- package/dist/sends/journal.d.ts.map +1 -1
- package/dist/sends/journal.js +62 -2
- package/dist/sends/journal.js.map +1 -1
- package/dist/sends/recipients.d.ts +3 -1
- package/dist/sends/recipients.d.ts.map +1 -1
- package/dist/sends/recipients.js +7 -2
- package/dist/sends/recipients.js.map +1 -1
- package/dist/server/lines.d.ts +8 -2
- package/dist/server/lines.d.ts.map +1 -1
- package/dist/server/lines.js +13 -2
- package/dist/server/lines.js.map +1 -1
- package/dist/server/server-connection.d.ts +9 -6
- package/dist/server/server-connection.d.ts.map +1 -1
- package/dist/server/server-connection.js +16 -7
- package/dist/server/server-connection.js.map +1 -1
- package/dist/server/server.d.ts +11 -4
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +132 -20
- package/dist/server/server.js.map +1 -1
- package/dist/server/start.d.ts +2 -0
- package/dist/server/start.d.ts.map +1 -1
- package/dist/server/start.js +8 -3
- package/dist/server/start.js.map +1 -1
- package/dist/session/handshake.d.ts +12 -1
- package/dist/session/handshake.d.ts.map +1 -1
- package/dist/session/handshake.js +4 -2
- package/dist/session/handshake.js.map +1 -1
- package/dist/session/prompt.d.ts.map +1 -1
- package/dist/session/prompt.js +7 -1
- package/dist/session/prompt.js.map +1 -1
- package/dist/session/store.d.ts +9 -2
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +19 -4
- package/dist/session/store.js.map +1 -1
- package/dist/spec/define.d.ts +12 -0
- package/dist/spec/define.d.ts.map +1 -1
- package/dist/spec/define.js.map +1 -1
- package/dist/spec/guards.d.ts +16 -0
- package/dist/spec/guards.d.ts.map +1 -0
- package/dist/spec/guards.js +30 -0
- package/dist/spec/guards.js.map +1 -0
- package/dist/spec/index.d.ts.map +1 -1
- package/dist/spec/index.js +6 -0
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/operations/account.d.ts.map +1 -1
- package/dist/spec/operations/account.js +3 -0
- package/dist/spec/operations/account.js.map +1 -1
- package/dist/spec/operations/assets.d.ts +10 -0
- package/dist/spec/operations/assets.d.ts.map +1 -0
- package/dist/spec/operations/assets.js +20 -0
- package/dist/spec/operations/assets.js.map +1 -0
- package/dist/spec/operations/attachments.d.ts.map +1 -1
- package/dist/spec/operations/attachments.js +2 -0
- package/dist/spec/operations/attachments.js.map +1 -1
- package/dist/spec/operations/banners.d.ts +7 -0
- package/dist/spec/operations/banners.d.ts.map +1 -0
- package/dist/spec/operations/banners.js +16 -0
- package/dist/spec/operations/banners.js.map +1 -0
- package/dist/spec/operations/calls.d.ts +8 -0
- package/dist/spec/operations/calls.d.ts.map +1 -0
- package/dist/spec/operations/calls.js +18 -0
- package/dist/spec/operations/calls.js.map +1 -0
- package/dist/spec/operations/chats.d.ts.map +1 -1
- package/dist/spec/operations/chats.js +46 -0
- package/dist/spec/operations/chats.js.map +1 -1
- package/dist/spec/operations/contacts.d.ts.map +1 -1
- package/dist/spec/operations/contacts.js +8 -0
- 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 +5 -0
- package/dist/spec/operations/folders.js.map +1 -1
- package/dist/spec/operations/login.d.ts.map +1 -1
- package/dist/spec/operations/login.js +6 -0
- package/dist/spec/operations/login.js.map +1 -1
- package/dist/spec/operations/messages.d.ts.map +1 -1
- package/dist/spec/operations/messages.js +50 -0
- package/dist/spec/operations/messages.js.map +1 -1
- package/dist/spec/operations/session.d.ts +5 -1
- package/dist/spec/operations/session.d.ts.map +1 -1
- package/dist/spec/operations/session.js +9 -1
- package/dist/spec/operations/session.js.map +1 -1
- package/dist/spec/operations/uploads.d.ts.map +1 -1
- package/dist/spec/operations/uploads.js +2 -0
- package/dist/spec/operations/uploads.js.map +1 -1
- package/dist/transcribe/index.d.ts.map +1 -1
- package/dist/transcribe/index.js +4 -1
- package/dist/transcribe/index.js.map +1 -1
- package/dist/transcribe/install.d.ts +3 -1
- package/dist/transcribe/install.d.ts.map +1 -1
- package/dist/transcribe/install.js +24 -1
- package/dist/transcribe/install.js.map +1 -1
- package/dist/upload.d.ts +3 -1
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +34 -4
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -11
- package/skills/max-cli/SKILL.md +6 -2
- package/dist/testing/mock-max.d.ts +0 -49
- package/dist/testing/mock-max.d.ts.map +0 -1
- package/dist/testing/mock-max.js +0 -80
- package/dist/testing/mock-max.js.map +0 -1
- package/dist/testing/sandbox.d.ts +0 -2
- package/dist/testing/sandbox.d.ts.map +0 -1
- package/dist/testing/sandbox.js +0 -36
- package/dist/testing/sandbox.js.map +0 -1
- package/dist/testing/unscripted.d.ts +0 -2
- package/dist/testing/unscripted.d.ts.map +0 -1
- package/dist/testing/unscripted.js +0 -18
- package/dist/testing/unscripted.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -13,6 +13,7 @@ import { tokenByQr } from "./session/login.js";
|
|
|
13
13
|
import { loginPausedUntil, withLoginRefused, withoutLoginPause, } from "./session/store.js";
|
|
14
14
|
import { WEB_USER_AGENT } from "./spec/identity.js";
|
|
15
15
|
import { buildRequest, checkResponse } from "./spec/index.js";
|
|
16
|
+
import { ASSET_TYPES } from "./spec/operations/assets.js";
|
|
16
17
|
import { isImage, readUpload, uploadFile, uploadPhoto } from "./upload.js";
|
|
17
18
|
/**
|
|
18
19
|
* **The only thing above this line that knows MAX exists.** Commands speak the domain model; the
|
|
@@ -30,6 +31,7 @@ export class MaxClient {
|
|
|
30
31
|
#store;
|
|
31
32
|
#connection;
|
|
32
33
|
#fullLogin;
|
|
34
|
+
#resume;
|
|
33
35
|
#warn;
|
|
34
36
|
#cache;
|
|
35
37
|
#offline;
|
|
@@ -42,9 +44,10 @@ export class MaxClient {
|
|
|
42
44
|
#previousCid = 0;
|
|
43
45
|
#people;
|
|
44
46
|
#merged;
|
|
45
|
-
constructor({ store, timeoutMs, connection, warn, cache, offline = false, events, sends, fullLogin = false, }) {
|
|
47
|
+
constructor({ store, timeoutMs, connection, warn, cache, offline = false, events, sends, fullLogin = false, resume, }) {
|
|
46
48
|
this.#store = store;
|
|
47
49
|
this.#fullLogin = fullLogin;
|
|
50
|
+
this.#resume = resume;
|
|
48
51
|
this.#connection = connection ?? new Connection(timeoutMs === undefined ? {} : { timeoutMs });
|
|
49
52
|
this.#warn = warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
50
53
|
this.#cache = cache;
|
|
@@ -93,7 +96,11 @@ export class MaxClient {
|
|
|
93
96
|
// The other devices are out from here on, whatever follows fails: the journal says so.
|
|
94
97
|
done();
|
|
95
98
|
const token = answer.token;
|
|
96
|
-
if (typeof token === "string" && token !== "") {
|
|
99
|
+
if (typeof token === "string" && token !== "" && this.#fromEnvironment()) {
|
|
100
|
+
this.#warnAbout("token_not_saved", "MAX gave this session a new token, and it was not saved: the one in use came from MAX_TOKEN — " +
|
|
101
|
+
"if MAX_TOKEN stops working, log in again and set it anew");
|
|
102
|
+
}
|
|
103
|
+
else if (typeof token === "string" && token !== "") {
|
|
97
104
|
try {
|
|
98
105
|
this.#store.writeToken(token);
|
|
99
106
|
}
|
|
@@ -189,6 +196,12 @@ export class MaxClient {
|
|
|
189
196
|
const cache = this.#cache;
|
|
190
197
|
return { ...chat, members: chat.kind === "channel" || !cache ? null : cache.chats.members(chat.id) };
|
|
191
198
|
},
|
|
199
|
+
/** The other person in a one-to-one chat, by the chat's participants; `undefined` for anything else. */
|
|
200
|
+
partner: async (chatId) => {
|
|
201
|
+
await this.#connectOnce();
|
|
202
|
+
const raw = asArray(this.#session().chats).find((chat) => asId(chat.id) === chatId);
|
|
203
|
+
return raw ? this.#partnerOf(raw) : undefined;
|
|
204
|
+
},
|
|
192
205
|
/** What a link leads to, without joining it. */
|
|
193
206
|
inspect: async (link) => {
|
|
194
207
|
if (this.#offline)
|
|
@@ -212,7 +225,7 @@ export class MaxClient {
|
|
|
212
225
|
markRead: async (chatId, messageId) => {
|
|
213
226
|
if (this.#offline)
|
|
214
227
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot mark a chat read");
|
|
215
|
-
this.#guard(chatId, "read", messageId);
|
|
228
|
+
this.#guard({ chatId, kind: "read" }, messageId);
|
|
216
229
|
try {
|
|
217
230
|
await this.#connectOnce();
|
|
218
231
|
const upTo = messageId ?? (await this.#history(chatId, { from: Date.now(), backward: 1, forward: 0 })).at(-1)?.id;
|
|
@@ -243,20 +256,23 @@ export class MaxClient {
|
|
|
243
256
|
* Not retried, unlike a message: a second attempt with a new `cid` is a second group, and
|
|
244
257
|
* whether MAX deduplicates a creation by `cid` is not measured.
|
|
245
258
|
*/
|
|
246
|
-
create: (title, people = []) =>
|
|
259
|
+
create: async (title, people = []) => {
|
|
247
260
|
const userIds = await this.#personIds(people);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
return this.#changeChat(null, "create", async () => {
|
|
262
|
+
const answer = await this.#wire.messages.send({
|
|
263
|
+
message: {
|
|
264
|
+
cid: this.#nextCid(),
|
|
265
|
+
attaches: [{ _type: "CONTROL", event: "new", chatType: "CHAT", title, userIds }],
|
|
266
|
+
},
|
|
267
|
+
notify: true,
|
|
268
|
+
});
|
|
269
|
+
const chat = toGroupCard(record(answer.chat) ?? {});
|
|
270
|
+
return { chatId: chat.id, result: chat, people: userIds.length };
|
|
271
|
+
}, userIds);
|
|
272
|
+
},
|
|
258
273
|
members: {
|
|
259
|
-
|
|
274
|
+
/** No history unless asked (`NEED-272`): what was said before somebody joined is not theirs by default. */
|
|
275
|
+
add: (reference, people, { history = false } = {}) => this.#updateMembers(reference, people, "members.add", { operation: "add", showHistory: history }),
|
|
260
276
|
remove: (reference, people) => this.#updateMembers(reference, people, "members.remove", { operation: "remove", cleanMsgPeriod: 0 }),
|
|
261
277
|
},
|
|
262
278
|
admins: {
|
|
@@ -684,20 +700,30 @@ export class MaxClient {
|
|
|
684
700
|
if (options.at !== undefined && options.cid !== undefined) {
|
|
685
701
|
throw new CliError("validation_error", `--cid repeats an ambiguous send, which is not safe for a scheduled one — \`max messages scheduled ${chatId}\` shows whether it is queued`);
|
|
686
702
|
}
|
|
703
|
+
// Read before the guard holds a place under the limit: a file that is refused sends nothing.
|
|
704
|
+
const files = await Promise.all((options.files ?? []).map(async (path) => ({
|
|
705
|
+
path,
|
|
706
|
+
bytes: await readUpload(path, { anyFile: options.anyFile === true }),
|
|
707
|
+
photo: isImage(path),
|
|
708
|
+
})));
|
|
709
|
+
// Measured 2026-09-24: photos share a message, but a file with anything beside it is refused `proto.payload`.
|
|
710
|
+
if (files.some((file) => !file.photo) && files.length > 1) {
|
|
711
|
+
throw new CliError("validation_error", "a file goes in a message of its own — photos can share one; send them apart");
|
|
712
|
+
}
|
|
687
713
|
// Before connecting: a refused send never opens a socket when the chat was given as an id.
|
|
688
714
|
try {
|
|
689
|
-
this.#sends?.check(
|
|
715
|
+
this.#sends?.check({
|
|
716
|
+
chatId,
|
|
717
|
+
kind: "message",
|
|
718
|
+
...(options.cid === undefined ? {} : { cid: options.cid }),
|
|
719
|
+
...(options.at === undefined ? {} : { scheduledFor: new Date(options.at).toISOString() }),
|
|
720
|
+
});
|
|
690
721
|
}
|
|
691
722
|
catch (error) {
|
|
692
723
|
this.#sends?.record({ chatId, outcome: "refused", errorCode: asCliError(error).code });
|
|
693
724
|
throw error;
|
|
694
725
|
}
|
|
695
726
|
const cid = options.cid ?? this.#nextCid();
|
|
696
|
-
const files = await Promise.all((options.files ?? []).map(async (path) => ({ path, bytes: await readUpload(path), photo: isImage(path) })));
|
|
697
|
-
// Measured 2026-09-24: photos share a message, but a file with anything beside it is refused `proto.payload`.
|
|
698
|
-
if (files.some((file) => !file.photo) && files.length > 1) {
|
|
699
|
-
throw new CliError("validation_error", "a file goes in a message of its own — photos can share one; send them apart");
|
|
700
|
-
}
|
|
701
727
|
const attachments = files.map(({ bytes, photo }) => ({
|
|
702
728
|
kind: photo ? "photo" : "file",
|
|
703
729
|
bytes: bytes.length,
|
|
@@ -740,7 +766,7 @@ export class MaxClient {
|
|
|
740
766
|
edit: async (chatId, messageId, text, { markdown = false } = {}) => {
|
|
741
767
|
if (this.#offline)
|
|
742
768
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot edit");
|
|
743
|
-
this.#guard(chatId, "edit", messageId);
|
|
769
|
+
this.#guard({ chatId, kind: "edit" }, messageId);
|
|
744
770
|
try {
|
|
745
771
|
await this.#connectOnce();
|
|
746
772
|
const lookup = { names: namesFrom(this.#session().contacts), ...viewer(this.#store) };
|
|
@@ -782,7 +808,7 @@ export class MaxClient {
|
|
|
782
808
|
forward: async (fromChatId, messageId, toChatId, options = {}) => {
|
|
783
809
|
if (this.#offline)
|
|
784
810
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot forward");
|
|
785
|
-
this.#guard(toChatId, "forward");
|
|
811
|
+
this.#guard({ chatId: toChatId, kind: "forward" });
|
|
786
812
|
const cid = options.cid ?? this.#nextCid();
|
|
787
813
|
try {
|
|
788
814
|
const sent = await this.#deliver(toChatId, "", cid, {
|
|
@@ -820,7 +846,7 @@ export class MaxClient {
|
|
|
820
846
|
throw new CliError("validation_error", `${messageIds.length} messages at once — at most ${DELETE_AT_ONCE} per call, so MAX sees deletions spread out`);
|
|
821
847
|
}
|
|
822
848
|
const count = messageIds.length;
|
|
823
|
-
this.#guard(chatId, "delete",
|
|
849
|
+
this.#guard({ chatId, kind: "delete", count });
|
|
824
850
|
try {
|
|
825
851
|
await this.#connectOnce();
|
|
826
852
|
await this.#wire.messages.delete({ chatId, messageIds, forMe: !forEveryone });
|
|
@@ -851,7 +877,7 @@ export class MaxClient {
|
|
|
851
877
|
pin: async (chatId, messageId, { notify = false } = {}) => {
|
|
852
878
|
if (this.#offline)
|
|
853
879
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot pin");
|
|
854
|
-
this.#guard(chatId, "pin", messageId ?? undefined);
|
|
880
|
+
this.#guard({ chatId, kind: "pin", notify }, messageId ?? undefined);
|
|
855
881
|
try {
|
|
856
882
|
await this.#connectOnce();
|
|
857
883
|
const known = asArray(this.#session().chats).find((raw) => asId(raw.id) === chatId);
|
|
@@ -859,11 +885,11 @@ export class MaxClient {
|
|
|
859
885
|
throw new CliError("validation_error", `chat ${chatId} is a personal chat — MAX pins only in groups and channels`);
|
|
860
886
|
}
|
|
861
887
|
await this.#wire.chats.update({ chatId, pinMessageId: messageId ?? "0", notifyPin: notify });
|
|
862
|
-
this.#sends?.record({ chatId, kind: "pin", outcome: "sent", ...(messageId ? { messageId } : {}) });
|
|
888
|
+
this.#sends?.record({ chatId, kind: "pin", notify, outcome: "sent", ...(messageId ? { messageId } : {}) });
|
|
863
889
|
return { chatId, pinned: messageId };
|
|
864
890
|
}
|
|
865
891
|
catch (error) {
|
|
866
|
-
this.#sends?.record({ chatId, kind: "pin", outcome: "failed", errorCode: asCliError(error).code });
|
|
892
|
+
this.#sends?.record({ chatId, kind: "pin", notify, outcome: "failed", errorCode: asCliError(error).code });
|
|
867
893
|
throw error;
|
|
868
894
|
}
|
|
869
895
|
},
|
|
@@ -982,7 +1008,7 @@ export class MaxClient {
|
|
|
982
1008
|
if (this.#offline)
|
|
983
1009
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot react");
|
|
984
1010
|
try {
|
|
985
|
-
this.#sends?.check(chatId, "reaction");
|
|
1011
|
+
this.#sends?.check({ chatId, kind: "reaction" });
|
|
986
1012
|
}
|
|
987
1013
|
catch (error) {
|
|
988
1014
|
this.#sends?.record({ chatId, kind: "reaction", outcome: "refused", errorCode: asCliError(error).code });
|
|
@@ -1000,14 +1026,16 @@ export class MaxClient {
|
|
|
1000
1026
|
}
|
|
1001
1027
|
}
|
|
1002
1028
|
/** Asks the send guard, and writes a refusal to the send journal before passing it on. */
|
|
1003
|
-
#guard(
|
|
1029
|
+
#guard(request, messageId) {
|
|
1030
|
+
const { chatId, kind, notify } = request;
|
|
1004
1031
|
try {
|
|
1005
|
-
this.#sends?.check(
|
|
1032
|
+
this.#sends?.check(request);
|
|
1006
1033
|
}
|
|
1007
1034
|
catch (error) {
|
|
1008
1035
|
this.#sends?.record({
|
|
1009
1036
|
chatId,
|
|
1010
|
-
kind,
|
|
1037
|
+
...(kind ? { kind } : {}),
|
|
1038
|
+
...(notify === undefined ? {} : { notify }),
|
|
1011
1039
|
outcome: "refused",
|
|
1012
1040
|
...(messageId ? { messageId } : {}),
|
|
1013
1041
|
errorCode: asCliError(error).code,
|
|
@@ -1091,6 +1119,25 @@ export class MaxClient {
|
|
|
1091
1119
|
* What a hidden web tab reports once, 20 s after it opened: the chat list, shown at `at`.
|
|
1092
1120
|
* `sessionId` is when the tab's connection began, and it survives the tab's reconnects.
|
|
1093
1121
|
*/
|
|
1122
|
+
/**
|
|
1123
|
+
* **The reads a web tab sends after every login** (`MAX-52`, recorded 2026-09-25), in its order
|
|
1124
|
+
* and all at once, as it sends them: folders, banners, call history, then the four asset sets.
|
|
1125
|
+
* Each carries the sync value its previous answer returned — 0 the first time — and the answers
|
|
1126
|
+
* are kept only for those. `max serve` alone sends them; a one-shot command never did.
|
|
1127
|
+
*/
|
|
1128
|
+
readLikeTab: async (sync) => {
|
|
1129
|
+
await this.#connectOnce();
|
|
1130
|
+
const folders = this.#wire.folders.list({ folderSync: sync.folders });
|
|
1131
|
+
const banners = this.#wire.banners.list({ bannersSync: 0 });
|
|
1132
|
+
const calls = this.#wire.calls.history({ callHistorySync: sync.calls });
|
|
1133
|
+
const assets = ASSET_TYPES.map((type) => this.#wire.assets.update({ type, sync: sync.assets[type] ?? 0 }));
|
|
1134
|
+
const [folderAnswer, , callAnswer, answers] = await Promise.all([folders, banners, calls, Promise.all(assets)]);
|
|
1135
|
+
return {
|
|
1136
|
+
folders: numberOr(folderAnswer.folderSync, sync.folders),
|
|
1137
|
+
calls: numberOr(callAnswer.callHistorySync, sync.calls),
|
|
1138
|
+
assets: Object.fromEntries(ASSET_TYPES.map((type, at) => [type, numberOr(answers[at]?.sync, sync.assets[type] ?? 0)])),
|
|
1139
|
+
};
|
|
1140
|
+
},
|
|
1094
1141
|
chatListShown: async ({ at, sessionId }) => {
|
|
1095
1142
|
const viewerId = this.#store.readState().viewerId;
|
|
1096
1143
|
if (!viewerId)
|
|
@@ -1114,6 +1161,19 @@ export class MaxClient {
|
|
|
1114
1161
|
* a command as that command's own login.
|
|
1115
1162
|
*/
|
|
1116
1163
|
snapshot: () => this.#session(),
|
|
1164
|
+
/**
|
|
1165
|
+
* What the next login on a new connection sends to pick up where this one is (`MAX-51`):
|
|
1166
|
+
* `undefined` when this login lacks what that needs, and the next one is then a full login.
|
|
1167
|
+
*/
|
|
1168
|
+
resumeFrom: () => {
|
|
1169
|
+
const login = this.#login;
|
|
1170
|
+
const configHash = record(login?.config)?.hash;
|
|
1171
|
+
if (!login || typeof login.time !== "number" || typeof configHash !== "string")
|
|
1172
|
+
return undefined;
|
|
1173
|
+
const chats = asArray(login.chats).map((chat) => record(chat) ?? {});
|
|
1174
|
+
const chatsSync = Math.max(0, ...chats.map(eventTime));
|
|
1175
|
+
return { login: { lastLogin: login.time, chatsSync, configHash }, chats };
|
|
1176
|
+
},
|
|
1117
1177
|
/**
|
|
1118
1178
|
* Keeps the snapshot current from one push. **`false` means it can no longer be trusted** and
|
|
1119
1179
|
* the server stops handing it out until it has logged in again. What web.max.ru does with each
|
|
@@ -1156,6 +1216,9 @@ export class MaxClient {
|
|
|
1156
1216
|
const at = chats.findIndex((candidate) => asId(candidate.id) === id);
|
|
1157
1217
|
if (at >= 0)
|
|
1158
1218
|
chats[at] = changed;
|
|
1219
|
+
// Past this, a new login rebuilds the list rather than pushes growing it without end.
|
|
1220
|
+
else if (chats.length >= LIVE_CHATS)
|
|
1221
|
+
return false;
|
|
1159
1222
|
else
|
|
1160
1223
|
chats.push(changed);
|
|
1161
1224
|
this.#session().chats = chats;
|
|
@@ -1175,17 +1238,47 @@ export class MaxClient {
|
|
|
1175
1238
|
*/
|
|
1176
1239
|
message: async (opcode, payload) => {
|
|
1177
1240
|
const raw = record(payload.message);
|
|
1241
|
+
if (opcode !== NEW_MESSAGE || !raw || raw.status !== undefined)
|
|
1242
|
+
return undefined;
|
|
1243
|
+
return this.#messageHit(payload);
|
|
1244
|
+
},
|
|
1245
|
+
/** An edit, a deletion or a reaction pushed by MAX, or `undefined` for anything else. */
|
|
1246
|
+
change: async (opcode, payload) => {
|
|
1178
1247
|
const chatId = asId(payload.chatId);
|
|
1179
|
-
if (
|
|
1248
|
+
if (chatId === undefined)
|
|
1180
1249
|
return undefined;
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1250
|
+
const raw = record(payload.message);
|
|
1251
|
+
if (opcode === NEW_MESSAGE && raw?.status === "EDITED") {
|
|
1252
|
+
const message = await this.#messageHit(payload);
|
|
1253
|
+
return message && { event: "edit", message };
|
|
1254
|
+
}
|
|
1255
|
+
const messageId = asId(opcode === NEW_MESSAGE ? raw?.id : payload.messageId);
|
|
1256
|
+
if (messageId === undefined)
|
|
1257
|
+
return undefined;
|
|
1258
|
+
if (opcode === NEW_MESSAGE && raw?.status === "REMOVED") {
|
|
1259
|
+
return { event: "delete", chatId, chatTitle: await this.#chatTitle(chatId), messageId };
|
|
1260
|
+
}
|
|
1261
|
+
if (opcode === REACTIONS_CHANGED) {
|
|
1262
|
+
const reactions = toReactions(payload);
|
|
1263
|
+
return { event: "reaction", chatId, chatTitle: await this.#chatTitle(chatId), messageId, reactions };
|
|
1264
|
+
}
|
|
1265
|
+
return undefined;
|
|
1187
1266
|
},
|
|
1188
1267
|
};
|
|
1268
|
+
async #messageHit(payload) {
|
|
1269
|
+
const raw = record(payload.message);
|
|
1270
|
+
const chatId = asId(payload.chatId);
|
|
1271
|
+
if (!raw || chatId === undefined)
|
|
1272
|
+
return undefined;
|
|
1273
|
+
const session = this.#session();
|
|
1274
|
+
const [message] = await this.#nameSenders([
|
|
1275
|
+
toMessage(raw, chatId, { names: namesFrom(session.contacts), ...viewer(this.#store) }),
|
|
1276
|
+
]);
|
|
1277
|
+
return message && { ...message, chatTitle: await this.#chatTitle(chatId) };
|
|
1278
|
+
}
|
|
1279
|
+
async #chatTitle(chatId) {
|
|
1280
|
+
return (await this.chats.list()).items.find((candidate) => candidate.id === chatId)?.title ?? null;
|
|
1281
|
+
}
|
|
1189
1282
|
/**
|
|
1190
1283
|
* Opens the connection and logs in with the stored token, or with one offered for trial.
|
|
1191
1284
|
*
|
|
@@ -1220,6 +1313,7 @@ export class MaxClient {
|
|
|
1220
1313
|
token,
|
|
1221
1314
|
deviceId: state.deviceId,
|
|
1222
1315
|
...(sync === undefined ? {} : { sync }),
|
|
1316
|
+
...(this.#resume ? { resume: this.#resume.login } : {}),
|
|
1223
1317
|
});
|
|
1224
1318
|
}
|
|
1225
1319
|
catch (error) {
|
|
@@ -1247,9 +1341,26 @@ export class MaxClient {
|
|
|
1247
1341
|
lastLoginAt: new Date().toISOString(),
|
|
1248
1342
|
});
|
|
1249
1343
|
this.#keepRotatedToken(token);
|
|
1250
|
-
|
|
1344
|
+
// `max serve` hands this login to any process that asks its socket; none of them needs the token.
|
|
1345
|
+
const { token: _, ...withoutToken } = this.#login;
|
|
1346
|
+
this.#login = withoutToken;
|
|
1347
|
+
if (this.#resume)
|
|
1348
|
+
this.#mergeChanged(this.#resume.chats);
|
|
1349
|
+
else
|
|
1350
|
+
await this.#readRestOfChats();
|
|
1251
1351
|
this.#mergeLogin(viewerId);
|
|
1252
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* A login with `chatsSync` answers only the chats that changed since it (measured 2026-09-20), so
|
|
1355
|
+
* they go over the list the previous connection held: same id replaced, the rest kept, newest first.
|
|
1356
|
+
*/
|
|
1357
|
+
#mergeChanged(before) {
|
|
1358
|
+
const session = this.#session();
|
|
1359
|
+
const changed = asArray(session.chats);
|
|
1360
|
+
const ids = new Set(changed.map((chat) => asId(record(chat)?.id)));
|
|
1361
|
+
const kept = before.filter((chat) => !ids.has(asId(chat.id)));
|
|
1362
|
+
session.chats = [...changed, ...kept].sort((a, b) => eventTime(b) - eventTime(a));
|
|
1363
|
+
}
|
|
1253
1364
|
/**
|
|
1254
1365
|
* **The chats LOGIN left out, as the web tab reads them:** one `CHATS_LIST` from the last-activity
|
|
1255
1366
|
* time of the oldest chat it sent, 2.3 s after the login answer (capture 2026-09-25). Measured the
|
|
@@ -1302,6 +1413,10 @@ export class MaxClient {
|
|
|
1302
1413
|
const rotated = this.#session().token;
|
|
1303
1414
|
if (typeof rotated !== "string" || rotated === "" || rotated === sent)
|
|
1304
1415
|
return;
|
|
1416
|
+
if (this.#fromEnvironment()) {
|
|
1417
|
+
this.#warnAbout("token_not_saved", "MAX refreshed the session; the fresh token was not saved, because the one in use came from MAX_TOKEN");
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1305
1420
|
try {
|
|
1306
1421
|
this.#store.writeToken(rotated);
|
|
1307
1422
|
}
|
|
@@ -1535,6 +1650,15 @@ export class MaxClient {
|
|
|
1535
1650
|
return answer;
|
|
1536
1651
|
}
|
|
1537
1652
|
/** The sentence to the person, and only the code to the log (`WarningEvent`). */
|
|
1653
|
+
/** A token from `MAX_TOKEN` belongs to whoever set it: nothing MAX hands back replaces what is stored. */
|
|
1654
|
+
#fromEnvironment() {
|
|
1655
|
+
try {
|
|
1656
|
+
return this.#store.tokenSource() === "environment";
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1538
1662
|
#warnAbout(code, message, extra = {}) {
|
|
1539
1663
|
this.#warn(message);
|
|
1540
1664
|
this.#emit({ event: "warning", code, ...extra });
|
|
@@ -1596,11 +1720,11 @@ export class MaxClient {
|
|
|
1596
1720
|
* Every change to a chat, guarded and journalled as a send is. Never retried: none of these is
|
|
1597
1721
|
* measured to be safe to repeat, and a repeated `create` is a second group.
|
|
1598
1722
|
*/
|
|
1599
|
-
async #changeChat(chatId, action, act) {
|
|
1723
|
+
async #changeChat(chatId, action, act, personIds) {
|
|
1600
1724
|
if (this.#offline)
|
|
1601
1725
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot change a chat");
|
|
1602
1726
|
try {
|
|
1603
|
-
this.#sends?.check(chatId, "chat", action);
|
|
1727
|
+
this.#sends?.check({ chatId, kind: "chat", action, ...(personIds ? { personIds } : {}) });
|
|
1604
1728
|
}
|
|
1605
1729
|
catch (error) {
|
|
1606
1730
|
this.#sends?.record({ chatId, kind: "chat", action, outcome: "refused", errorCode: asCliError(error).code });
|
|
@@ -1632,8 +1756,8 @@ export class MaxClient {
|
|
|
1632
1756
|
}
|
|
1633
1757
|
async #updateMembers(reference, people, action, change) {
|
|
1634
1758
|
const chatId = await this.chats.resolve(reference);
|
|
1759
|
+
const userIds = await this.#personIds(people);
|
|
1635
1760
|
return this.#changeChat(chatId, action, async () => {
|
|
1636
|
-
const userIds = await this.#personIds(people);
|
|
1637
1761
|
const answer = await this.#wire.chats.updateMembers({ chatId, userIds, ...change });
|
|
1638
1762
|
const chat = record(answer.chat);
|
|
1639
1763
|
return {
|
|
@@ -1641,7 +1765,7 @@ export class MaxClient {
|
|
|
1641
1765
|
people: userIds.length,
|
|
1642
1766
|
result: { chatId, action, people: userIds, chat: chat ? toGroupCard(chat) : null },
|
|
1643
1767
|
};
|
|
1644
|
-
});
|
|
1768
|
+
}, action === "members.add" ? userIds : undefined);
|
|
1645
1769
|
}
|
|
1646
1770
|
/** An id goes as given; a name is looked up in the store, and an ambiguous one is refused. */
|
|
1647
1771
|
async #personIds(references) {
|
|
@@ -1728,7 +1852,7 @@ export class MaxClient {
|
|
|
1728
1852
|
if (this.#offline)
|
|
1729
1853
|
throw new CliError("validation_error", "`--offline` reads what was recorded; it cannot change the account");
|
|
1730
1854
|
try {
|
|
1731
|
-
this.#sends?.check(null, "account", action);
|
|
1855
|
+
this.#sends?.check({ chatId: null, kind: "account", action });
|
|
1732
1856
|
}
|
|
1733
1857
|
catch (error) {
|
|
1734
1858
|
this.#sends?.record({
|
|
@@ -1816,6 +1940,8 @@ export const ADMIN_RIGHTS = {
|
|
|
1816
1940
|
delete: 1024,
|
|
1817
1941
|
};
|
|
1818
1942
|
/** PyMax asks for this many; paging join requests is not known. */
|
|
1943
|
+
/** The most chats `max serve` holds from pushes before it logs in again instead. */
|
|
1944
|
+
const LIVE_CHATS = 10_000;
|
|
1819
1945
|
const JOIN_REQUESTS = 100;
|
|
1820
1946
|
/**
|
|
1821
1947
|
* A private link goes as `join/<token>`, whatever came before it, as PyMax sends it and as
|
|
@@ -1879,6 +2005,14 @@ export const timeOfMessageId = (id) => {
|
|
|
1879
2005
|
* account rarely has that many chats change between two checks, and the rest are named, not lost.
|
|
1880
2006
|
*/
|
|
1881
2007
|
const INBOX_CHATS = 20;
|
|
2008
|
+
export const FIRST_TAB_SYNC = { folders: 0, calls: 0, assets: {} };
|
|
2009
|
+
const numberOr = (value, fallback) => (typeof value === "number" ? value : fallback);
|
|
2010
|
+
const eventTime = (chat) => {
|
|
2011
|
+
const time = record(chat)?.lastEventTime;
|
|
2012
|
+
return typeof time === "number" ? time : 0;
|
|
2013
|
+
};
|
|
2014
|
+
/** Reactions on a message changed; PyMax calls it `NOTIF_MSG_REACTIONS_CHANGED` (tab recording 2026-09-25). */
|
|
2015
|
+
const REACTIONS_CHANGED = 155;
|
|
1882
2016
|
/** MAX pushes this when a message arrives in any chat. PyMax calls it `NOTIF_MESSAGE`. */
|
|
1883
2017
|
const NEW_MESSAGE = 128;
|
|
1884
2018
|
/** Bounded so that deleting a long history is many spread-out calls, never one sweep (`MAX-47`). */
|