@mu-cabin/muc-cli 0.2.2 → 0.3.1
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 +53 -11
- package/dist/muc.mjs +845 -83
- package/package.json +1 -1
package/dist/muc.mjs
CHANGED
|
@@ -21288,6 +21288,7 @@ import { join } from "node:path";
|
|
|
21288
21288
|
var DIR = join(homedir(), ".muc");
|
|
21289
21289
|
var FILE = join(DIR, "config.json");
|
|
21290
21290
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
21291
|
+
var CHAT_OS_VALUES = ["Windows", "OS X", "Mac", "iOS", "Android"];
|
|
21291
21292
|
var MucConfigSchema = external_exports.object({
|
|
21292
21293
|
appKey: external_exports.string().regex(/^[0-9a-f]{8}$/, "appKey must be 8 lowercase hex chars"),
|
|
21293
21294
|
appId: external_exports.string().regex(/^[0-9a-f]{24}$/, "appId must be 24 lowercase hex chars"),
|
|
@@ -21307,6 +21308,10 @@ var MucConfigSchema = external_exports.object({
|
|
|
21307
21308
|
// any other → that literal string (e.g. "\n— bot")
|
|
21308
21309
|
// Per-call `--tail` / `--no-tail` flags on `muc send` override this.
|
|
21309
21310
|
messageTail: external_exports.string().max(200).optional(),
|
|
21311
|
+
// Default chat-channel `os` slot for TCP commands when --os is not passed.
|
|
21312
|
+
// PC ({Windows, OS X, Mac}) vs Mobile ({iOS, Android}); same slot as the
|
|
21313
|
+
// running desktop will kick it. Built-in fallback is 'iOS' (Mobile).
|
|
21314
|
+
defaultChatOs: external_exports.enum(CHAT_OS_VALUES).optional(),
|
|
21310
21315
|
savedAt: external_exports.number().int().nonnegative(),
|
|
21311
21316
|
schemaVersion: external_exports.literal(CONFIG_SCHEMA_VERSION)
|
|
21312
21317
|
});
|
|
@@ -21394,6 +21399,47 @@ function saveCredentials(c) {
|
|
|
21394
21399
|
}
|
|
21395
21400
|
var credentialsPath = FILE2;
|
|
21396
21401
|
|
|
21402
|
+
// src/lib/constants.ts
|
|
21403
|
+
var APP_NAME = "MUC";
|
|
21404
|
+
var APP_VERSION = "3.3.1-250925001";
|
|
21405
|
+
var APP_VERSION_IOS = "3.3.0301";
|
|
21406
|
+
var PLATFORM_IOS = 1;
|
|
21407
|
+
var PLATFORM_ANDROID = 2;
|
|
21408
|
+
var PLATFORM_WIN = 4;
|
|
21409
|
+
var PLATFORM_MAC = 5;
|
|
21410
|
+
var SECURE_SYS_KEY = "mx_app_key";
|
|
21411
|
+
var PWD_DES_IV = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
21412
|
+
var DEFAULT_CHAT_OS = "iOS";
|
|
21413
|
+
var DEFAULT_REST_PLATFORM = PLATFORM_IOS;
|
|
21414
|
+
function defaultMessageTail(userName) {
|
|
21415
|
+
const name = userName.trim() || "unknown";
|
|
21416
|
+
return `
|
|
21417
|
+
\u2014\u2014 \u6765\u81EA${name}\u7684muc-cli`;
|
|
21418
|
+
}
|
|
21419
|
+
var DEFAULTS = {
|
|
21420
|
+
// Hosts/ports for the 东航 deployment. Other tenants override at init.
|
|
21421
|
+
restHost: "https://muc.ceair.com/",
|
|
21422
|
+
ssoHost: "https://store.ceair.com/",
|
|
21423
|
+
imHost: "muc.ceair.com",
|
|
21424
|
+
imPort: 8101,
|
|
21425
|
+
fileHost: "muc.ceair.com",
|
|
21426
|
+
filePort: 6101
|
|
21427
|
+
};
|
|
21428
|
+
function apiUrls(cfg) {
|
|
21429
|
+
const host = cfg.restHost.endsWith("/") ? cfg.restHost : `${cfg.restHost}/`;
|
|
21430
|
+
return {
|
|
21431
|
+
SYS: `${host}sysconfig/v5/app`,
|
|
21432
|
+
MUC: `${host}muc/v5/app`,
|
|
21433
|
+
MAM: `${host}mam/v5/app`,
|
|
21434
|
+
MAS: `${host}mas/v5/app`,
|
|
21435
|
+
MPM: `${host}mpm/v5/app`,
|
|
21436
|
+
IMM: `${host}imm/v5/app`,
|
|
21437
|
+
MRM: `${host}mrm/v5/app`,
|
|
21438
|
+
CONTACT: `${host}contacts/v5/app`,
|
|
21439
|
+
SECURE: `${host}contacts/secure/v5/app`
|
|
21440
|
+
};
|
|
21441
|
+
}
|
|
21442
|
+
|
|
21397
21443
|
// src/lib/cli.ts
|
|
21398
21444
|
var SCHEMA_VERSION = 1;
|
|
21399
21445
|
var EXIT_CODES = {
|
|
@@ -21403,6 +21449,8 @@ var EXIT_CODES = {
|
|
|
21403
21449
|
NOT_FOUND: 4,
|
|
21404
21450
|
REMOTE_ERROR: 5,
|
|
21405
21451
|
NETWORK: 5,
|
|
21452
|
+
TIMEOUT: 5,
|
|
21453
|
+
KICKED: 5,
|
|
21406
21454
|
CONFIG_MISSING: 6,
|
|
21407
21455
|
CONFIG_INVALID: 7,
|
|
21408
21456
|
INTERNAL: 1
|
|
@@ -21420,6 +21468,9 @@ var JSON_MODE = false;
|
|
|
21420
21468
|
function setJsonMode(v) {
|
|
21421
21469
|
JSON_MODE = v;
|
|
21422
21470
|
}
|
|
21471
|
+
function isJsonMode() {
|
|
21472
|
+
return JSON_MODE;
|
|
21473
|
+
}
|
|
21423
21474
|
function stableStringify(value) {
|
|
21424
21475
|
return JSON.stringify(value, (_k, v) => {
|
|
21425
21476
|
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
@@ -21568,6 +21619,11 @@ function parseChatOs(raw) {
|
|
|
21568
21619
|
"expected windows|osx|mac|ios|android"
|
|
21569
21620
|
);
|
|
21570
21621
|
}
|
|
21622
|
+
function resolveChatOs(optsOs, cfg) {
|
|
21623
|
+
if (optsOs !== void 0) return optsOs;
|
|
21624
|
+
if (cfg.defaultChatOs !== void 0) return cfg.defaultChatOs;
|
|
21625
|
+
return DEFAULT_CHAT_OS;
|
|
21626
|
+
}
|
|
21571
21627
|
function parseMoment(label) {
|
|
21572
21628
|
return (raw) => {
|
|
21573
21629
|
if (/^\d+$/.test(raw)) {
|
|
@@ -21591,6 +21647,24 @@ function requireSingleTarget(opts, label) {
|
|
|
21591
21647
|
);
|
|
21592
21648
|
}
|
|
21593
21649
|
}
|
|
21650
|
+
function defineStream(program3, spec) {
|
|
21651
|
+
const cmd = program3.command(spec.name).description(spec.description);
|
|
21652
|
+
spec.configure?.(cmd);
|
|
21653
|
+
cmd.action(async () => {
|
|
21654
|
+
try {
|
|
21655
|
+
await spec.run(cmd);
|
|
21656
|
+
} catch (e) {
|
|
21657
|
+
if (e instanceof CliError) fail(e.code, e.message, e.hint);
|
|
21658
|
+
const cls = classifyError(e);
|
|
21659
|
+
fail(cls.code, cls.msg, cls.hint);
|
|
21660
|
+
}
|
|
21661
|
+
});
|
|
21662
|
+
REGISTRY.push({
|
|
21663
|
+
name: spec.name,
|
|
21664
|
+
description: spec.description,
|
|
21665
|
+
output_schema: spec.streamSchema ?? { type: "object", description: "NDJSON-stream command \u2014 one JSON object per stdout line" }
|
|
21666
|
+
});
|
|
21667
|
+
}
|
|
21594
21668
|
function define2(program3, spec) {
|
|
21595
21669
|
const cmd = program3.command(spec.name).description(spec.description);
|
|
21596
21670
|
spec.configure?.(cmd);
|
|
@@ -21694,47 +21768,6 @@ function getDeviceName() {
|
|
|
21694
21768
|
return `${hostname()} (muc-cli)`;
|
|
21695
21769
|
}
|
|
21696
21770
|
|
|
21697
|
-
// src/lib/constants.ts
|
|
21698
|
-
var APP_NAME = "MUC";
|
|
21699
|
-
var APP_VERSION = "3.3.1-250925001";
|
|
21700
|
-
var APP_VERSION_IOS = "3.3.0301";
|
|
21701
|
-
var PLATFORM_IOS = 1;
|
|
21702
|
-
var PLATFORM_ANDROID = 2;
|
|
21703
|
-
var PLATFORM_WIN = 4;
|
|
21704
|
-
var PLATFORM_MAC = 5;
|
|
21705
|
-
var SECURE_SYS_KEY = "mx_app_key";
|
|
21706
|
-
var PWD_DES_IV = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
21707
|
-
var DEFAULT_CHAT_OS = "iOS";
|
|
21708
|
-
var DEFAULT_REST_PLATFORM = PLATFORM_IOS;
|
|
21709
|
-
function defaultMessageTail(userName) {
|
|
21710
|
-
const name = userName.trim() || "unknown";
|
|
21711
|
-
return `
|
|
21712
|
-
\u2014\u2014 \u6765\u81EA${name}\u7684muc-cli`;
|
|
21713
|
-
}
|
|
21714
|
-
var DEFAULTS = {
|
|
21715
|
-
// Hosts/ports for the 东航 deployment. Other tenants override at init.
|
|
21716
|
-
restHost: "https://muc.ceair.com/",
|
|
21717
|
-
ssoHost: "https://store.ceair.com/",
|
|
21718
|
-
imHost: "muc.ceair.com",
|
|
21719
|
-
imPort: 8101,
|
|
21720
|
-
fileHost: "muc.ceair.com",
|
|
21721
|
-
filePort: 6101
|
|
21722
|
-
};
|
|
21723
|
-
function apiUrls(cfg) {
|
|
21724
|
-
const host = cfg.restHost.endsWith("/") ? cfg.restHost : `${cfg.restHost}/`;
|
|
21725
|
-
return {
|
|
21726
|
-
SYS: `${host}sysconfig/v5/app`,
|
|
21727
|
-
MUC: `${host}muc/v5/app`,
|
|
21728
|
-
MAM: `${host}mam/v5/app`,
|
|
21729
|
-
MAS: `${host}mas/v5/app`,
|
|
21730
|
-
MPM: `${host}mpm/v5/app`,
|
|
21731
|
-
IMM: `${host}imm/v5/app`,
|
|
21732
|
-
MRM: `${host}mrm/v5/app`,
|
|
21733
|
-
CONTACT: `${host}contacts/v5/app`,
|
|
21734
|
-
SECURE: `${host}contacts/secure/v5/app`
|
|
21735
|
-
};
|
|
21736
|
-
}
|
|
21737
|
-
|
|
21738
21771
|
// src/lib/http.ts
|
|
21739
21772
|
var UA = `MUC/${APP_VERSION} (CLI; muc-cli/${CLI_VERSION})`;
|
|
21740
21773
|
var SENSITIVE_QS = /* @__PURE__ */ new Set(["accessToken", "tokenPwd", "sign"]);
|
|
@@ -22113,6 +22146,8 @@ var FingerprintShape = external_exports.object({
|
|
|
22113
22146
|
filePort: external_exports.number(),
|
|
22114
22147
|
messageTail: external_exports.string(),
|
|
22115
22148
|
// summary: "default" / "none" / "custom (N chars)"
|
|
22149
|
+
defaultChatOs: external_exports.string(),
|
|
22150
|
+
// summary: explicit value or "default (iOS)"
|
|
22116
22151
|
savedAt: external_exports.number(),
|
|
22117
22152
|
schemaVersion: external_exports.literal(CONFIG_SCHEMA_VERSION)
|
|
22118
22153
|
});
|
|
@@ -22138,6 +22173,10 @@ function summarizeTail(t) {
|
|
|
22138
22173
|
if (t === "") return "none";
|
|
22139
22174
|
return `custom (${t.length} chars)`;
|
|
22140
22175
|
}
|
|
22176
|
+
function summarizeChatOs(v) {
|
|
22177
|
+
if (v === void 0) return `default (${DEFAULT_CHAT_OS})`;
|
|
22178
|
+
return v;
|
|
22179
|
+
}
|
|
22141
22180
|
function redactConfig(c) {
|
|
22142
22181
|
return {
|
|
22143
22182
|
appKey: fingerprint2(c.appKey),
|
|
@@ -22152,6 +22191,7 @@ function redactConfig(c) {
|
|
|
22152
22191
|
fileHost: c.fileHost,
|
|
22153
22192
|
filePort: c.filePort,
|
|
22154
22193
|
messageTail: summarizeTail(c.messageTail),
|
|
22194
|
+
defaultChatOs: summarizeChatOs(c.defaultChatOs),
|
|
22155
22195
|
savedAt: c.savedAt,
|
|
22156
22196
|
schemaVersion: c.schemaVersion
|
|
22157
22197
|
};
|
|
@@ -22297,7 +22337,7 @@ function register2(program3) {
|
|
|
22297
22337
|
description: "Bootstrap or update ~/.muc/config.json (tenant config: APP_KEY, APP_ID, signing secrets, hosts/ports). Interactive prompts by default; non-interactive via flags or MUC_* env vars. WARNING: env vars are visible to other processes via /proc/<pid>/environ and shell history \u2014 prefer interactive or flag-based for production.",
|
|
22298
22338
|
output: Output2,
|
|
22299
22339
|
configure: (cmd) => {
|
|
22300
|
-
cmd.option("--check", "verify config presence and validity (exit 0 ok / 6 missing / 7 invalid); does not modify").option("--show", "print the saved config (secrets are always redacted in both human and JSON output; cat ~/.muc/config.json for raw values)").option("--reset", "delete ~/.muc/config.json (requires --yes)").option("--yes", "confirm a destructive action like --reset").option("--no-interactive", "fail rather than prompt for missing fields").option("--app-key <hex>", "tenant APP_KEY (8 lowercase hex)").option("--app-id <hex>", "tenant APP_ID (24 lowercase hex)").option("--sign-secret-rest <s>", "REST signing secret").option("--sign-secret-secure <s>", "secure-API signing secret").option("--pwd-des-key <s>", "password DES key (defaults to APP_KEY)").option("--rest-host <url>", "REST host URL").option("--sso-host <url>", "SSO host URL (optional)").option("--im-host <h>", "IM TCP host").option("--im-port <n>", "IM TCP port").option("--file-host <h>", "file-channel TCP host").option("--file-port <n>", "file-channel TCP port").option("--message-tail <text>", "custom signature appended to outbound `muc send` text (max 200 chars)").option("--no-message-tail", "explicitly disable the message tail (no signature appended)");
|
|
22340
|
+
cmd.option("--check", "verify config presence and validity (exit 0 ok / 6 missing / 7 invalid); does not modify").option("--show", "print the saved config (secrets are always redacted in both human and JSON output; cat ~/.muc/config.json for raw values)").option("--reset", "delete ~/.muc/config.json (requires --yes)").option("--yes", "confirm a destructive action like --reset").option("--no-interactive", "fail rather than prompt for missing fields").option("--app-key <hex>", "tenant APP_KEY (8 lowercase hex)").option("--app-id <hex>", "tenant APP_ID (24 lowercase hex)").option("--sign-secret-rest <s>", "REST signing secret").option("--sign-secret-secure <s>", "secure-API signing secret").option("--pwd-des-key <s>", "password DES key (defaults to APP_KEY)").option("--rest-host <url>", "REST host URL").option("--sso-host <url>", "SSO host URL (optional)").option("--im-host <h>", "IM TCP host").option("--im-port <n>", "IM TCP port").option("--file-host <h>", "file-channel TCP host").option("--file-port <n>", "file-channel TCP port").option("--message-tail <text>", "custom signature appended to outbound `muc send` text (max 200 chars)").option("--no-message-tail", "explicitly disable the message tail (no signature appended)").option("--default-os <os>", "default chat-channel os slot for TCP commands when --os is not passed (windows|osx|mac|ios|android); built-in fallback is iOS");
|
|
22301
22341
|
},
|
|
22302
22342
|
run: async (cmd) => {
|
|
22303
22343
|
const opts = cmd.opts();
|
|
@@ -22407,6 +22447,26 @@ function register2(program3) {
|
|
|
22407
22447
|
} else if (existing && "messageTail" in existing) {
|
|
22408
22448
|
messageTail = existing.messageTail;
|
|
22409
22449
|
}
|
|
22450
|
+
let defaultChatOs;
|
|
22451
|
+
let dcoRaw;
|
|
22452
|
+
const dcoFlag = opts.defaultOs;
|
|
22453
|
+
const dcoEnv = process.env.MUC_DEFAULT_OS;
|
|
22454
|
+
if (typeof dcoFlag === "string" && dcoFlag.length > 0) {
|
|
22455
|
+
dcoRaw = dcoFlag;
|
|
22456
|
+
} else if (dcoEnv !== void 0 && dcoEnv.length > 0) {
|
|
22457
|
+
dcoRaw = dcoEnv;
|
|
22458
|
+
} else if (existing?.defaultChatOs !== void 0) {
|
|
22459
|
+
defaultChatOs = existing.defaultChatOs;
|
|
22460
|
+
} else if (interactive) {
|
|
22461
|
+
const answer = await readLine(
|
|
22462
|
+
"Default chat OS slot (windows|osx|mac|ios|android)",
|
|
22463
|
+
DEFAULT_CHAT_OS.toLowerCase()
|
|
22464
|
+
);
|
|
22465
|
+
if (answer.length > 0) dcoRaw = answer;
|
|
22466
|
+
}
|
|
22467
|
+
if (dcoRaw !== void 0) {
|
|
22468
|
+
defaultChatOs = parseChatOs(dcoRaw);
|
|
22469
|
+
}
|
|
22410
22470
|
const draft = {
|
|
22411
22471
|
appKey: String(partial.appKey ?? ""),
|
|
22412
22472
|
appId: String(partial.appId ?? ""),
|
|
@@ -22420,6 +22480,7 @@ function register2(program3) {
|
|
|
22420
22480
|
fileHost: String(partial.fileHost ?? ""),
|
|
22421
22481
|
filePort: Number(partial.filePort ?? 0),
|
|
22422
22482
|
messageTail,
|
|
22483
|
+
defaultChatOs,
|
|
22423
22484
|
savedAt: Date.now(),
|
|
22424
22485
|
schemaVersion: CONFIG_SCHEMA_VERSION
|
|
22425
22486
|
};
|
|
@@ -22835,17 +22896,20 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22835
22896
|
host;
|
|
22836
22897
|
port;
|
|
22837
22898
|
ackTimeoutMs;
|
|
22899
|
+
idleTimeoutMs;
|
|
22838
22900
|
debug;
|
|
22839
22901
|
sock = null;
|
|
22840
22902
|
reader;
|
|
22841
22903
|
sq = 2;
|
|
22842
22904
|
pending = /* @__PURE__ */ new Map();
|
|
22843
22905
|
closed = false;
|
|
22906
|
+
idleTimer = null;
|
|
22844
22907
|
constructor(opts) {
|
|
22845
22908
|
super();
|
|
22846
22909
|
this.host = opts.host;
|
|
22847
22910
|
this.port = opts.port;
|
|
22848
22911
|
this.ackTimeoutMs = opts.ackTimeoutMs ?? 25e3;
|
|
22912
|
+
this.idleTimeoutMs = opts.idleTimeoutMs ?? 75e3;
|
|
22849
22913
|
this.debug = !!opts.debug;
|
|
22850
22914
|
this.reader = new FrameReader((f) => this.handleFrame(f));
|
|
22851
22915
|
}
|
|
@@ -22855,12 +22919,20 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22855
22919
|
const sock = createConnection({ host: this.host, port: this.port });
|
|
22856
22920
|
this.sock = sock;
|
|
22857
22921
|
sock.setKeepAlive(true, 2e4);
|
|
22922
|
+
const errorSink = (_e) => {
|
|
22923
|
+
};
|
|
22924
|
+
this.on("error", errorSink);
|
|
22858
22925
|
const onErr = (e) => {
|
|
22859
22926
|
cleanup();
|
|
22860
22927
|
reject(e);
|
|
22861
22928
|
};
|
|
22929
|
+
const onPreReadyClose = () => {
|
|
22930
|
+
cleanup();
|
|
22931
|
+
reject(new Error("chat socket closed before negotiate-ack"));
|
|
22932
|
+
};
|
|
22862
22933
|
const cleanup = () => {
|
|
22863
22934
|
sock.off("error", onErr);
|
|
22935
|
+
sock.off("close", onPreReadyClose);
|
|
22864
22936
|
this.off("ready", onReady);
|
|
22865
22937
|
};
|
|
22866
22938
|
const onReady = () => {
|
|
@@ -22868,26 +22940,56 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22868
22940
|
resolve2();
|
|
22869
22941
|
};
|
|
22870
22942
|
sock.once("error", onErr);
|
|
22943
|
+
sock.once("close", onPreReadyClose);
|
|
22871
22944
|
this.once("ready", onReady);
|
|
22872
22945
|
sock.on("connect", () => {
|
|
22873
22946
|
if (this.debug) console.error("[wire] tcp connect");
|
|
22874
22947
|
this.emit("connect");
|
|
22948
|
+
this.armIdle();
|
|
22875
22949
|
this.sendNegotiate();
|
|
22876
22950
|
});
|
|
22877
|
-
sock.on("data", (buf) =>
|
|
22878
|
-
|
|
22879
|
-
|
|
22880
|
-
if (this.closed) return;
|
|
22881
|
-
this.closed = true;
|
|
22882
|
-
for (const { reject: reject2, timer } of this.pending.values()) {
|
|
22883
|
-
clearTimeout(timer);
|
|
22884
|
-
reject2(new Error("socket closed"));
|
|
22885
|
-
}
|
|
22886
|
-
this.pending.clear();
|
|
22887
|
-
this.emit("close");
|
|
22951
|
+
sock.on("data", (buf) => {
|
|
22952
|
+
this.armIdle();
|
|
22953
|
+
this.reader.feed(buf);
|
|
22888
22954
|
});
|
|
22955
|
+
sock.on("error", (e) => this.emit("error", e));
|
|
22956
|
+
sock.on("close", () => this.teardown(new Error("socket closed")));
|
|
22889
22957
|
});
|
|
22890
22958
|
}
|
|
22959
|
+
/**
|
|
22960
|
+
* (Re)arm the inactivity watchdog. Called every time inbound bytes land
|
|
22961
|
+
* (data callback) or any active confirmation. If no traffic arrives for
|
|
22962
|
+
* `idleTimeoutMs`, we synthesize a teardown so commands don't hang on a
|
|
22963
|
+
* half-open socket the kernel hasn't surfaced yet.
|
|
22964
|
+
*/
|
|
22965
|
+
armIdle() {
|
|
22966
|
+
if (this.closed) return;
|
|
22967
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
22968
|
+
this.idleTimer = setTimeout(() => {
|
|
22969
|
+
if (this.closed) return;
|
|
22970
|
+
this.emit("error", new Error(`chat socket idle for ${Math.round(this.idleTimeoutMs / 1e3)}s \u2014 tearing down`));
|
|
22971
|
+
try {
|
|
22972
|
+
this.sock?.destroy();
|
|
22973
|
+
} catch {
|
|
22974
|
+
}
|
|
22975
|
+
this.teardown(new Error("idle timeout"));
|
|
22976
|
+
}, this.idleTimeoutMs);
|
|
22977
|
+
}
|
|
22978
|
+
/** Single owner of close-side cleanup: clears idle timer, rejects all pending sends, emits 'close' once. */
|
|
22979
|
+
teardown(reason) {
|
|
22980
|
+
if (this.closed) return;
|
|
22981
|
+
this.closed = true;
|
|
22982
|
+
if (this.idleTimer) {
|
|
22983
|
+
clearTimeout(this.idleTimer);
|
|
22984
|
+
this.idleTimer = null;
|
|
22985
|
+
}
|
|
22986
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
22987
|
+
clearTimeout(timer);
|
|
22988
|
+
reject(reason);
|
|
22989
|
+
}
|
|
22990
|
+
this.pending.clear();
|
|
22991
|
+
this.emit("close");
|
|
22992
|
+
}
|
|
22891
22993
|
sendNegotiate() {
|
|
22892
22994
|
const frame = buildFrame(T.NEGOTIATE, NEGOTIATE_BODY, NEGOTIATE_CRC);
|
|
22893
22995
|
if (this.debug) console.error("[wire] \u2192 NEGOTIATE", frame.toString("hex"));
|
|
@@ -22926,11 +23028,26 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22926
23028
|
if (!this.sock || this.closed) throw new Error("not connected");
|
|
22927
23029
|
this.sock.write(buildFrame(type, payload, crcOverride));
|
|
22928
23030
|
}
|
|
23031
|
+
/**
|
|
23032
|
+
* Send an unsolicited frame that *re-uses* a sq from a server-pushed frame
|
|
23033
|
+
* (rather than allocating a new one via nextSq). This is how the desktop
|
|
23034
|
+
* SDK ACKs inbound `talk/msg` (message-server/modules/message.js:961-984):
|
|
23035
|
+
* it echoes the original frame's sq back with `{sid, cid, data:{tids}}`.
|
|
23036
|
+
* Fire-and-forget — no pending registration, no response expected.
|
|
23037
|
+
*/
|
|
23038
|
+
sendAck(sid, cid, sq, data = {}) {
|
|
23039
|
+
if (!this.sock || this.closed) throw new Error("not connected");
|
|
23040
|
+
const obj = { sid, cid, sq, data };
|
|
23041
|
+
const payload = aesEncrypt(JSON.stringify(obj));
|
|
23042
|
+
if (this.debug) console.error(`[wire] \u2192 ACK ${sid}/${cid} sq=${sq}`);
|
|
23043
|
+
this.sock.write(buildFrame(T.AES_JSON, payload));
|
|
23044
|
+
}
|
|
22929
23045
|
close() {
|
|
22930
23046
|
if (!this.sock || this.closed) return;
|
|
22931
|
-
|
|
23047
|
+
const sock = this.sock;
|
|
23048
|
+
this.teardown(new Error("socket closed by client"));
|
|
22932
23049
|
try {
|
|
22933
|
-
|
|
23050
|
+
sock.end();
|
|
22934
23051
|
} catch {
|
|
22935
23052
|
}
|
|
22936
23053
|
}
|
|
@@ -22940,10 +23057,10 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22940
23057
|
try {
|
|
22941
23058
|
bodyText = aesDecrypt(f.payload);
|
|
22942
23059
|
} catch (e) {
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
console.error(`[wire]
|
|
23060
|
+
const err = new Error(`aes decrypt failed (type=${f.type} len=${f.payload.length}): ${e.message}`);
|
|
23061
|
+
this.emit("error", err);
|
|
23062
|
+
if (this.listenerCount("error") <= 1) {
|
|
23063
|
+
console.error(`[wire] ${err.message}`);
|
|
22947
23064
|
}
|
|
22948
23065
|
return;
|
|
22949
23066
|
}
|
|
@@ -23141,13 +23258,14 @@ function register9(program3) {
|
|
|
23141
23258
|
description: 'Search groups the current user belongs to (TCP chat channel sid="team", cid="search_own_teams"). Pass --enrich for names + member counts. Will kick a session in the same online-slot.',
|
|
23142
23259
|
output: Output9,
|
|
23143
23260
|
configure: (cmd) => {
|
|
23144
|
-
cmd.argument("<keyword>", "team name fragment").option("--enrich", "follow up with team/get_team per id").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
23261
|
+
cmd.argument("<keyword>", "team name fragment").option("--enrich", "follow up with team/get_team per id").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
23145
23262
|
},
|
|
23146
23263
|
run: async (cmd) => {
|
|
23147
23264
|
const [kw] = cmd.args;
|
|
23148
|
-
const { enrich, os, debug } = cmd.opts();
|
|
23265
|
+
const { enrich, os: optsOs, debug } = cmd.opts();
|
|
23149
23266
|
const cfg = requireConfig();
|
|
23150
23267
|
const cred = requireAuth(loadCredentials());
|
|
23268
|
+
const os = resolveChatOs(optsOs, cfg);
|
|
23151
23269
|
const session = await openChatSession(cred, { os, debug });
|
|
23152
23270
|
const { socket } = session;
|
|
23153
23271
|
try {
|
|
@@ -23333,11 +23451,13 @@ function register13(program3) {
|
|
|
23333
23451
|
description: "After chat-login, run sync/syncDone, sync/syncSession, sync/syncWithdraws and return the raw responses.",
|
|
23334
23452
|
output: Output13,
|
|
23335
23453
|
configure: (cmd) => {
|
|
23336
|
-
cmd.option("--os <os>", "chat-channel os slot", parseChatOs
|
|
23454
|
+
cmd.option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
23337
23455
|
},
|
|
23338
23456
|
run: async (cmd) => {
|
|
23339
|
-
const { os, debug } = cmd.opts();
|
|
23457
|
+
const { os: optsOs, debug } = cmd.opts();
|
|
23458
|
+
const cfg = requireConfig();
|
|
23340
23459
|
const cred = requireAuth(loadCredentials());
|
|
23460
|
+
const os = resolveChatOs(optsOs, cfg);
|
|
23341
23461
|
const session = await openChatSession(cred, { os, debug });
|
|
23342
23462
|
const { socket } = session;
|
|
23343
23463
|
try {
|
|
@@ -23591,7 +23711,7 @@ function register15(program3) {
|
|
|
23591
23711
|
description: "Send a plain-text chat message via the TCP chat channel. VISIBLE TO THE RECIPIENT \u2014 non-self targets require --yes-not-self.",
|
|
23592
23712
|
output: Output15,
|
|
23593
23713
|
configure: (cmd) => {
|
|
23594
|
-
cmd.argument("<text...>", "message text (variadic; words rejoined with spaces)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
23714
|
+
cmd.argument("<text...>", "message text (variadic; words rejoined with spaces)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--tail <text>", "override message tail for this call only").option("--no-tail", "do not append a message tail for this call").option("--debug", "forward wire-level debug events to stderr");
|
|
23595
23715
|
},
|
|
23596
23716
|
run: async (cmd) => {
|
|
23597
23717
|
const text = cmd.args.join(" ").trim();
|
|
@@ -23599,6 +23719,7 @@ function register15(program3) {
|
|
|
23599
23719
|
const opts = cmd.opts();
|
|
23600
23720
|
const cfg = requireConfig();
|
|
23601
23721
|
const cred = requireAuth(loadCredentials());
|
|
23722
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
23602
23723
|
requireSingleTarget(opts, "send");
|
|
23603
23724
|
const conflict = detectFlagConflict(process.argv.slice(2), "tail");
|
|
23604
23725
|
if (conflict.sawValueForm && conflict.sawNegatedForm) {
|
|
@@ -23637,7 +23758,7 @@ function register15(program3) {
|
|
|
23637
23758
|
subType: 1,
|
|
23638
23759
|
body: finalText,
|
|
23639
23760
|
fName: cred.name,
|
|
23640
|
-
platform:
|
|
23761
|
+
platform: os === "Mac" || os === "OS X" ? "Mac" : "Windows",
|
|
23641
23762
|
appKey: cfg.appKey
|
|
23642
23763
|
});
|
|
23643
23764
|
if (opts.dryRun) {
|
|
@@ -23649,7 +23770,7 @@ function register15(program3) {
|
|
|
23649
23770
|
frame: { sid: "talk", cid: "sendMsg", data: { msg } }
|
|
23650
23771
|
};
|
|
23651
23772
|
}
|
|
23652
|
-
const session = await openChatSession(cred, { os
|
|
23773
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
23653
23774
|
const { socket } = session;
|
|
23654
23775
|
let res;
|
|
23655
23776
|
try {
|
|
@@ -24389,7 +24510,7 @@ function register16(program3) {
|
|
|
24389
24510
|
description: "Upload a local file (TCP 6101) and broadcast as MESSAGE_CHAT_FILE (subType=7). VISIBLE \u2014 non-self targets require --yes-not-self.",
|
|
24390
24511
|
output: Output16,
|
|
24391
24512
|
configure: (cmd) => {
|
|
24392
|
-
cmd.argument("<file>", "absolute or relative path to a local file").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "resolve target + sanity-check the local file but do not upload or send").option("--os <os>", "chat-channel os slot for the talk/sendMsg", parseChatOs
|
|
24513
|
+
cmd.argument("<file>", "absolute or relative path to a local file").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "resolve target + sanity-check the local file but do not upload or send").option("--os <os>", "chat-channel os slot for the talk/sendMsg (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
24393
24514
|
},
|
|
24394
24515
|
run: async (cmd) => {
|
|
24395
24516
|
const filePath = path2.resolve(String(cmd.args[0] ?? ""));
|
|
@@ -24397,6 +24518,7 @@ function register16(program3) {
|
|
|
24397
24518
|
const opts = cmd.opts();
|
|
24398
24519
|
const cfg = requireConfig();
|
|
24399
24520
|
const cred = requireAuth(loadCredentials());
|
|
24521
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
24400
24522
|
requireSingleTarget(opts, "send-file");
|
|
24401
24523
|
let scene;
|
|
24402
24524
|
let toId;
|
|
@@ -24447,10 +24569,10 @@ function register16(program3) {
|
|
|
24447
24569
|
expiredDay: up.expiredDay
|
|
24448
24570
|
}),
|
|
24449
24571
|
fName: cred.name,
|
|
24450
|
-
platform:
|
|
24572
|
+
platform: os === "Mac" || os === "OS X" ? "Mac" : "Windows",
|
|
24451
24573
|
appKey: cfg.appKey
|
|
24452
24574
|
});
|
|
24453
|
-
const session = await openChatSession(cred, { os
|
|
24575
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
24454
24576
|
let res;
|
|
24455
24577
|
try {
|
|
24456
24578
|
res = await session.socket.send("talk", "sendMsg", { msg });
|
|
@@ -24563,13 +24685,14 @@ function register17(program3) {
|
|
|
24563
24685
|
description: "Upload an image (TCP 6101) and broadcast as MESSAGE_CHAT_IMAGE (subType=8). VISIBLE \u2014 non-self targets require --yes-not-self.",
|
|
24564
24686
|
output: Output17,
|
|
24565
24687
|
configure: (cmd) => {
|
|
24566
|
-
cmd.argument("<file>", "PNG/JPEG/GIF/BMP image path").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default)").option("--yes-not-self", "required for non-self targets").option("--width <px>", "override probed image width", parsePosInt("--width")).option("--height <px>", "override probed image height", parsePosInt("--height")).option("--dry-run", "sanity-check the local file (probe + stat) but do not upload or send").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
24688
|
+
cmd.argument("<file>", "PNG/JPEG/GIF/BMP image path").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default)").option("--yes-not-self", "required for non-self targets").option("--width <px>", "override probed image width", parsePosInt("--width")).option("--height <px>", "override probed image height", parsePosInt("--height")).option("--dry-run", "sanity-check the local file (probe + stat) but do not upload or send").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
24567
24689
|
},
|
|
24568
24690
|
run: async (cmd) => {
|
|
24569
24691
|
const filePath = path3.resolve(String(cmd.args[0] ?? ""));
|
|
24570
24692
|
const opts = cmd.opts();
|
|
24571
24693
|
const cfg = requireConfig();
|
|
24572
24694
|
const cred = requireAuth(loadCredentials());
|
|
24695
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
24573
24696
|
requireSingleTarget(opts, "send-image");
|
|
24574
24697
|
let scene;
|
|
24575
24698
|
let toId;
|
|
@@ -24635,10 +24758,10 @@ function register17(program3) {
|
|
|
24635
24758
|
height
|
|
24636
24759
|
}),
|
|
24637
24760
|
fName: cred.name,
|
|
24638
|
-
platform:
|
|
24761
|
+
platform: os === "Mac" || os === "OS X" ? "Mac" : "Windows",
|
|
24639
24762
|
appKey: cfg.appKey
|
|
24640
24763
|
});
|
|
24641
|
-
const session = await openChatSession(cred, { os
|
|
24764
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
24642
24765
|
let res;
|
|
24643
24766
|
try {
|
|
24644
24767
|
res = await session.socket.send("talk", "sendMsg", { msg });
|
|
@@ -24717,13 +24840,14 @@ function register18(program3) {
|
|
|
24717
24840
|
description: "Upload an audio clip (TCP 6101) and broadcast as MESSAGE_CHAT_AUDIO (subType=9). VISIBLE \u2014 non-self targets require --yes-not-self.",
|
|
24718
24841
|
output: Output18,
|
|
24719
24842
|
configure: (cmd) => {
|
|
24720
|
-
cmd.argument("<file>", "audio file path (typically .amr)").requiredOption("--duration <ms>", "clip duration in milliseconds", parsePosInt("--duration")).option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default)").option("--yes-not-self", "required for non-self targets").option("--dry-run", "sanity-check the local file but do not upload or send").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
24843
|
+
cmd.argument("<file>", "audio file path (typically .amr)").requiredOption("--duration <ms>", "clip duration in milliseconds", parsePosInt("--duration")).option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default)").option("--yes-not-self", "required for non-self targets").option("--dry-run", "sanity-check the local file but do not upload or send").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
24721
24844
|
},
|
|
24722
24845
|
run: async (cmd) => {
|
|
24723
24846
|
const filePath = path4.resolve(String(cmd.args[0] ?? ""));
|
|
24724
24847
|
const opts = cmd.opts();
|
|
24725
24848
|
const cfg = requireConfig();
|
|
24726
24849
|
const cred = requireAuth(loadCredentials());
|
|
24850
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
24727
24851
|
requireSingleTarget(opts, "send-voice");
|
|
24728
24852
|
let scene;
|
|
24729
24853
|
let toId;
|
|
@@ -24782,10 +24906,10 @@ function register18(program3) {
|
|
|
24782
24906
|
duration: opts.duration
|
|
24783
24907
|
}),
|
|
24784
24908
|
fName: cred.name,
|
|
24785
|
-
platform:
|
|
24909
|
+
platform: os === "Mac" || os === "OS X" ? "Mac" : "Windows",
|
|
24786
24910
|
appKey: cfg.appKey
|
|
24787
24911
|
});
|
|
24788
|
-
const session = await openChatSession(cred, { os
|
|
24912
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
24789
24913
|
let res;
|
|
24790
24914
|
try {
|
|
24791
24915
|
res = await session.socket.send("talk", "sendMsg", { msg });
|
|
@@ -24954,12 +25078,13 @@ function register20(program3) {
|
|
|
24954
25078
|
description: "Withdraw a previously-sent message (P2P_MSG_CANCEL). VISIBLE TO THE RECIPIENT \u2014 non-self targets require --yes-not-self.",
|
|
24955
25079
|
output: Output20,
|
|
24956
25080
|
configure: (cmd) => {
|
|
24957
|
-
cmd.requiredOption("--mid <mid>", "message id to withdraw").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "withdraw in your self-chat (default if no target)").option("--yes-not-self", "required to confirm withdrawing in a non-self conversation").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
25081
|
+
cmd.requiredOption("--mid <mid>", "message id to withdraw").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "withdraw in your self-chat (default if no target)").option("--yes-not-self", "required to confirm withdrawing in a non-self conversation").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
24958
25082
|
},
|
|
24959
25083
|
run: async (cmd) => {
|
|
24960
25084
|
const opts = cmd.opts();
|
|
24961
25085
|
const cfg = requireConfig();
|
|
24962
25086
|
const cred = requireAuth(loadCredentials());
|
|
25087
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
24963
25088
|
requireSingleTarget(opts, "withdraw");
|
|
24964
25089
|
let scene;
|
|
24965
25090
|
let toId;
|
|
@@ -24989,7 +25114,7 @@ function register20(program3) {
|
|
|
24989
25114
|
subType: 2,
|
|
24990
25115
|
body: { mids: [opts.mid] },
|
|
24991
25116
|
fName: cred.name,
|
|
24992
|
-
platform:
|
|
25117
|
+
platform: os === "Mac" || os === "OS X" ? "Mac" : "Windows",
|
|
24993
25118
|
appKey: cfg.appKey
|
|
24994
25119
|
});
|
|
24995
25120
|
if (opts.dryRun) {
|
|
@@ -25001,7 +25126,7 @@ function register20(program3) {
|
|
|
25001
25126
|
frame: { sid: "talk", cid: "sendMsg", data: { msg } }
|
|
25002
25127
|
};
|
|
25003
25128
|
}
|
|
25004
|
-
const session = await openChatSession(cred, { os
|
|
25129
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
25005
25130
|
const { socket } = session;
|
|
25006
25131
|
let res;
|
|
25007
25132
|
try {
|
|
@@ -25044,11 +25169,13 @@ function register21(program3) {
|
|
|
25044
25169
|
description: "Send user/delSession to drop the local session record on all of the user's devices.",
|
|
25045
25170
|
output: Output21,
|
|
25046
25171
|
configure: (cmd) => {
|
|
25047
|
-
cmd.option("--peer <uid>", "session with a peer uid").option("--team <id>", "session with a team id").option("--to-self", "self-chat session (default if no target)").option("--yes-not-self", "required to confirm clearing a non-self session").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot", parseChatOs
|
|
25172
|
+
cmd.option("--peer <uid>", "session with a peer uid").option("--team <id>", "session with a team id").option("--to-self", "self-chat session (default if no target)").option("--yes-not-self", "required to confirm clearing a non-self session").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
25048
25173
|
},
|
|
25049
25174
|
run: async (cmd) => {
|
|
25050
25175
|
const opts = cmd.opts();
|
|
25176
|
+
const cfg = requireConfig();
|
|
25051
25177
|
const cred = requireAuth(loadCredentials());
|
|
25178
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
25052
25179
|
requireSingleTarget(opts, "clear-session");
|
|
25053
25180
|
let scene;
|
|
25054
25181
|
let toId;
|
|
@@ -25075,7 +25202,7 @@ function register21(program3) {
|
|
|
25075
25202
|
if (opts.dryRun) {
|
|
25076
25203
|
return { scene, toId, sId, dryRun: true, frame };
|
|
25077
25204
|
}
|
|
25078
|
-
const session = await openChatSession(cred, { os
|
|
25205
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
25079
25206
|
const { socket } = session;
|
|
25080
25207
|
let res;
|
|
25081
25208
|
try {
|
|
@@ -25091,8 +25218,638 @@ function register21(program3) {
|
|
|
25091
25218
|
});
|
|
25092
25219
|
}
|
|
25093
25220
|
|
|
25094
|
-
// src/
|
|
25221
|
+
// src/lib/reactions.ts
|
|
25222
|
+
async function saveReply(cfg, args) {
|
|
25223
|
+
const { ctx, stickerId, senderName, isFromMe, selfAppKey } = args;
|
|
25224
|
+
const isGroup = ctx.scene === "team_g";
|
|
25225
|
+
const targetApp = ctx.targetApp ?? selfAppKey;
|
|
25226
|
+
const body = {
|
|
25227
|
+
sid: ctx.sid,
|
|
25228
|
+
mid: ctx.mid,
|
|
25229
|
+
appkey: selfAppKey,
|
|
25230
|
+
toId: isGroup ? ctx.sid : isFromMe ? ctx.peerUid : ctx.srcSenderId,
|
|
25231
|
+
toAppKey: isGroup ? targetApp : isFromMe ? targetApp : ctx.srcSenderApp || ctx.peerApp,
|
|
25232
|
+
groupId: isGroup ? ctx.sid : void 0,
|
|
25233
|
+
stickerId,
|
|
25234
|
+
senderName,
|
|
25235
|
+
srcSenderId: ctx.srcSenderId,
|
|
25236
|
+
srcSenderApp: ctx.srcSenderApp
|
|
25237
|
+
};
|
|
25238
|
+
return await request({
|
|
25239
|
+
method: "POST",
|
|
25240
|
+
auth: true,
|
|
25241
|
+
url: `${apiUrls(cfg).IMM}/emoticon/reply/save`,
|
|
25242
|
+
body,
|
|
25243
|
+
headers: { "Content-Type": "application/json" }
|
|
25244
|
+
});
|
|
25245
|
+
}
|
|
25246
|
+
async function listReplies(cfg, mid) {
|
|
25247
|
+
return await request({
|
|
25248
|
+
method: "GET",
|
|
25249
|
+
auth: true,
|
|
25250
|
+
url: `${apiUrls(cfg).IMM}/emoticon/reply/list`,
|
|
25251
|
+
qs: { mid }
|
|
25252
|
+
});
|
|
25253
|
+
}
|
|
25254
|
+
async function revokeReply(cfg, args) {
|
|
25255
|
+
const { emoticonId, ctx, isFromMe, selfAppKey } = args;
|
|
25256
|
+
const targetApp = ctx.targetApp ?? selfAppKey;
|
|
25257
|
+
const body = {
|
|
25258
|
+
emoticonId,
|
|
25259
|
+
appKey: selfAppKey
|
|
25260
|
+
};
|
|
25261
|
+
if (ctx.scene === "p2p") {
|
|
25262
|
+
body.toId = isFromMe ? ctx.peerUid : ctx.srcSenderId;
|
|
25263
|
+
body.toAppKey = isFromMe ? targetApp : ctx.srcSenderApp || ctx.peerApp;
|
|
25264
|
+
} else {
|
|
25265
|
+
body.groupId = ctx.sid;
|
|
25266
|
+
body.toAppKey = targetApp;
|
|
25267
|
+
}
|
|
25268
|
+
return await request({
|
|
25269
|
+
method: "POST",
|
|
25270
|
+
auth: true,
|
|
25271
|
+
url: `${apiUrls(cfg).IMM}/emoticon/reply/revocation`,
|
|
25272
|
+
body,
|
|
25273
|
+
headers: { "Content-Type": "application/json" }
|
|
25274
|
+
});
|
|
25275
|
+
}
|
|
25276
|
+
|
|
25277
|
+
// src/commands/react.ts
|
|
25095
25278
|
var Output22 = external_exports.object({
|
|
25279
|
+
scene: external_exports.enum(["p2p", "team_g"]),
|
|
25280
|
+
sId: external_exports.string(),
|
|
25281
|
+
mid: external_exports.string(),
|
|
25282
|
+
stickerId: external_exports.string(),
|
|
25283
|
+
emoticonId: external_exports.string().optional(),
|
|
25284
|
+
raw: external_exports.unknown()
|
|
25285
|
+
});
|
|
25286
|
+
function register22(program3) {
|
|
25287
|
+
define2(program3, {
|
|
25288
|
+
name: "react",
|
|
25289
|
+
description: "Add an emoticon reaction to a message (POST emoticon/reply/save). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
|
|
25290
|
+
output: Output22,
|
|
25291
|
+
configure: (cmd) => {
|
|
25292
|
+
cmd.requiredOption("--mid <mid>", "message id to react to").requiredOption("--sticker-id <id>", "emoticon/sticker id (run `muc emoticons` to list packages)").option("--peer <uid>", "1:1 chat with a peer uid (the OTHER side of the conversation)").option("--team <id>", "group chat with a team id").option("--to-self", "react in your self-chat (default if no target)").option("--src-sender-id <uid>", "UID of the ORIGINAL message sender (defaults to current user \u2014 i.e. reacting to your own message)").option("--src-sender-app <appkey>", "appKey of the original message sender (defaults to your app key)").option("--target-app-key <appkey>", "original message `app_key` field \u2014 distinct from --src-sender-app (`fApp`); only matters in cross-tenant reacts. Defaults to your app key.").option("--sender-name <name>", "override displayed sender name (defaults to your saved name)").option("--yes-not-self", "required to confirm reacting in a non-self conversation");
|
|
25293
|
+
},
|
|
25294
|
+
run: async (cmd) => {
|
|
25295
|
+
const opts = cmd.opts();
|
|
25296
|
+
const cfg = requireConfig();
|
|
25297
|
+
const cred = requireAuth(loadCredentials());
|
|
25298
|
+
requireSingleTarget(opts, "react");
|
|
25299
|
+
let scene;
|
|
25300
|
+
let peerUid;
|
|
25301
|
+
if (opts.team) {
|
|
25302
|
+
scene = "team_g";
|
|
25303
|
+
} else if (opts.toSelf || !opts.peer && !opts.team) {
|
|
25304
|
+
scene = "p2p";
|
|
25305
|
+
peerUid = cred.uid;
|
|
25306
|
+
} else {
|
|
25307
|
+
scene = "p2p";
|
|
25308
|
+
peerUid = opts.peer.toUpperCase();
|
|
25309
|
+
}
|
|
25310
|
+
const isSelf = scene === "p2p" && peerUid === cred.uid;
|
|
25311
|
+
if (!isSelf && !opts.yesNotSelf) {
|
|
25312
|
+
throw new CliError(
|
|
25313
|
+
"USAGE",
|
|
25314
|
+
`refusing to react in ${scene}:${opts.team ?? peerUid} without --yes-not-self`,
|
|
25315
|
+
"add --yes-not-self to confirm \u2014 reactions are visible to all participants"
|
|
25316
|
+
);
|
|
25317
|
+
}
|
|
25318
|
+
const srcSenderId = (opts.srcSenderId ?? cred.uid).toUpperCase();
|
|
25319
|
+
const srcSenderApp = opts.srcSenderApp ?? cfg.appKey;
|
|
25320
|
+
const isFromMe = srcSenderId === cred.uid && srcSenderApp === cfg.appKey;
|
|
25321
|
+
const sid = scene === "team_g" ? opts.team : buildSId("p2p", cred.uid, peerUid);
|
|
25322
|
+
const ctx = {
|
|
25323
|
+
scene,
|
|
25324
|
+
sid,
|
|
25325
|
+
mid: opts.mid,
|
|
25326
|
+
srcSenderId,
|
|
25327
|
+
srcSenderApp,
|
|
25328
|
+
peerUid,
|
|
25329
|
+
peerApp: cfg.appKey,
|
|
25330
|
+
targetApp: opts.targetAppKey
|
|
25331
|
+
};
|
|
25332
|
+
const res = await saveReply(cfg, {
|
|
25333
|
+
ctx,
|
|
25334
|
+
stickerId: opts.stickerId,
|
|
25335
|
+
senderName: opts.senderName ?? cred.name ?? cred.uid,
|
|
25336
|
+
isFromMe,
|
|
25337
|
+
selfAppKey: cfg.appKey
|
|
25338
|
+
});
|
|
25339
|
+
if (res.code !== 0) {
|
|
25340
|
+
throw new CliError("REMOTE_ERROR", `react rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
|
|
25341
|
+
}
|
|
25342
|
+
const data = res.data ?? {};
|
|
25343
|
+
const emoticonId = pickEmoticonId(data);
|
|
25344
|
+
return {
|
|
25345
|
+
scene,
|
|
25346
|
+
sId: sid,
|
|
25347
|
+
mid: opts.mid,
|
|
25348
|
+
stickerId: opts.stickerId,
|
|
25349
|
+
emoticonId,
|
|
25350
|
+
raw: res
|
|
25351
|
+
};
|
|
25352
|
+
}
|
|
25353
|
+
});
|
|
25354
|
+
}
|
|
25355
|
+
function pickEmoticonId(data) {
|
|
25356
|
+
if (!data || typeof data !== "object") return void 0;
|
|
25357
|
+
const d = data;
|
|
25358
|
+
for (const k of ["emoticonId", "id", "replyId", "commentId"]) {
|
|
25359
|
+
const v = d[k];
|
|
25360
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
25361
|
+
if (typeof v === "number") return String(v);
|
|
25362
|
+
}
|
|
25363
|
+
return void 0;
|
|
25364
|
+
}
|
|
25365
|
+
|
|
25366
|
+
// src/commands/reactions.ts
|
|
25367
|
+
var Output23 = external_exports.object({
|
|
25368
|
+
mid: external_exports.string(),
|
|
25369
|
+
count: external_exports.number(),
|
|
25370
|
+
reactions: external_exports.array(external_exports.unknown()),
|
|
25371
|
+
raw: external_exports.unknown()
|
|
25372
|
+
});
|
|
25373
|
+
function register23(program3) {
|
|
25374
|
+
define2(program3, {
|
|
25375
|
+
name: "reactions",
|
|
25376
|
+
description: "List emoticon reactions on a message (GET emoticon/reply/list?mid=\u2026).",
|
|
25377
|
+
output: Output23,
|
|
25378
|
+
configure: (cmd) => {
|
|
25379
|
+
cmd.requiredOption("--mid <mid>", "message id to inspect");
|
|
25380
|
+
},
|
|
25381
|
+
run: async (cmd) => {
|
|
25382
|
+
const { mid } = cmd.opts();
|
|
25383
|
+
const cfg = requireConfig();
|
|
25384
|
+
requireAuth(loadCredentials());
|
|
25385
|
+
const res = await listReplies(cfg, mid);
|
|
25386
|
+
if (res.code !== 0) {
|
|
25387
|
+
throw new CliError("REMOTE_ERROR", `reactions list rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
|
|
25388
|
+
}
|
|
25389
|
+
const reactions = extractList(res.data);
|
|
25390
|
+
return {
|
|
25391
|
+
mid,
|
|
25392
|
+
count: reactions.length,
|
|
25393
|
+
reactions,
|
|
25394
|
+
raw: res
|
|
25395
|
+
};
|
|
25396
|
+
}
|
|
25397
|
+
});
|
|
25398
|
+
}
|
|
25399
|
+
function extractList(data) {
|
|
25400
|
+
if (Array.isArray(data)) return data;
|
|
25401
|
+
if (data && typeof data === "object") {
|
|
25402
|
+
for (const k of ["list", "reactions", "replies", "records", "items"]) {
|
|
25403
|
+
const v = data[k];
|
|
25404
|
+
if (Array.isArray(v)) return v;
|
|
25405
|
+
}
|
|
25406
|
+
}
|
|
25407
|
+
return [];
|
|
25408
|
+
}
|
|
25409
|
+
|
|
25410
|
+
// src/commands/unreact.ts
|
|
25411
|
+
var Output24 = external_exports.object({
|
|
25412
|
+
scene: external_exports.enum(["p2p", "team_g"]),
|
|
25413
|
+
emoticonId: external_exports.string(),
|
|
25414
|
+
raw: external_exports.unknown()
|
|
25415
|
+
});
|
|
25416
|
+
function register24(program3) {
|
|
25417
|
+
define2(program3, {
|
|
25418
|
+
name: "unreact",
|
|
25419
|
+
description: "Revoke one of your emoticon reactions (POST emoticon/reply/revocation). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
|
|
25420
|
+
output: Output24,
|
|
25421
|
+
configure: (cmd) => {
|
|
25422
|
+
cmd.requiredOption("--emoticon-id <id>", "reaction id (returned by `muc react` or seen in `muc reactions --mid \u2026`)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "revoke in your self-chat (default if no target)").option("--src-sender-id <uid>", "UID of the original message sender (defaults to current user)").option("--src-sender-app <appkey>", "appKey of the original message sender (defaults to your app key)").option("--target-app-key <appkey>", "original message `app_key` field \u2014 distinct from --src-sender-app (`fApp`); only matters in cross-tenant revokes. Defaults to your app key.").option("--yes-not-self", "required to confirm revoking in a non-self conversation");
|
|
25423
|
+
},
|
|
25424
|
+
run: async (cmd) => {
|
|
25425
|
+
const opts = cmd.opts();
|
|
25426
|
+
const cfg = requireConfig();
|
|
25427
|
+
const cred = requireAuth(loadCredentials());
|
|
25428
|
+
requireSingleTarget(opts, "unreact");
|
|
25429
|
+
let scene;
|
|
25430
|
+
let peerUid;
|
|
25431
|
+
if (opts.team) {
|
|
25432
|
+
scene = "team_g";
|
|
25433
|
+
} else if (opts.toSelf || !opts.peer && !opts.team) {
|
|
25434
|
+
scene = "p2p";
|
|
25435
|
+
peerUid = cred.uid;
|
|
25436
|
+
} else {
|
|
25437
|
+
scene = "p2p";
|
|
25438
|
+
peerUid = opts.peer.toUpperCase();
|
|
25439
|
+
}
|
|
25440
|
+
const isSelf = scene === "p2p" && peerUid === cred.uid;
|
|
25441
|
+
if (!isSelf && !opts.yesNotSelf) {
|
|
25442
|
+
throw new CliError(
|
|
25443
|
+
"USAGE",
|
|
25444
|
+
`refusing to revoke reaction in ${scene}:${opts.team ?? peerUid} without --yes-not-self`,
|
|
25445
|
+
"add --yes-not-self to confirm"
|
|
25446
|
+
);
|
|
25447
|
+
}
|
|
25448
|
+
const srcSenderId = (opts.srcSenderId ?? cred.uid).toUpperCase();
|
|
25449
|
+
const srcSenderApp = opts.srcSenderApp ?? cfg.appKey;
|
|
25450
|
+
const isFromMe = srcSenderId === cred.uid && srcSenderApp === cfg.appKey;
|
|
25451
|
+
const sid = scene === "team_g" ? opts.team : buildSId("p2p", cred.uid, peerUid);
|
|
25452
|
+
const ctx = {
|
|
25453
|
+
scene,
|
|
25454
|
+
sid,
|
|
25455
|
+
// mid not needed for revoke (server identifies by emoticonId)
|
|
25456
|
+
mid: "",
|
|
25457
|
+
srcSenderId,
|
|
25458
|
+
srcSenderApp,
|
|
25459
|
+
peerUid,
|
|
25460
|
+
peerApp: cfg.appKey,
|
|
25461
|
+
targetApp: opts.targetAppKey
|
|
25462
|
+
};
|
|
25463
|
+
const res = await revokeReply(cfg, {
|
|
25464
|
+
emoticonId: opts.emoticonId,
|
|
25465
|
+
ctx,
|
|
25466
|
+
isFromMe,
|
|
25467
|
+
selfAppKey: cfg.appKey
|
|
25468
|
+
});
|
|
25469
|
+
if (res.code !== 0) {
|
|
25470
|
+
throw new CliError("REMOTE_ERROR", `unreact rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
|
|
25471
|
+
}
|
|
25472
|
+
return {
|
|
25473
|
+
scene,
|
|
25474
|
+
emoticonId: opts.emoticonId,
|
|
25475
|
+
raw: res
|
|
25476
|
+
};
|
|
25477
|
+
}
|
|
25478
|
+
});
|
|
25479
|
+
}
|
|
25480
|
+
|
|
25481
|
+
// src/commands/wait-react.ts
|
|
25482
|
+
var Output25 = external_exports.object({
|
|
25483
|
+
matched: external_exports.literal(true),
|
|
25484
|
+
mid: external_exports.string(),
|
|
25485
|
+
reaction: external_exports.unknown()
|
|
25486
|
+
});
|
|
25487
|
+
function register25(program3) {
|
|
25488
|
+
define2(program3, {
|
|
25489
|
+
name: "wait-react",
|
|
25490
|
+
description: "Block until someone adds an emoticon reaction (subType=300) to --mid. Consumes a TCP slot \u2014 defaults to --os iOS (Mobile slot).",
|
|
25491
|
+
output: Output25,
|
|
25492
|
+
configure: (cmd) => {
|
|
25493
|
+
cmd.requiredOption("--mid <mid>", "message id to wait for a reaction on").option("--from-uid <uid>", "only consider reactions from this UID (case-insensitive match against fId of the type=3 notification)").option("--sticker-id <id>", "only consider reactions whose stickerId matches").option("--timeout <seconds>", "give up after this many seconds (default 300)", parsePosInt("--timeout"), 300).option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
25494
|
+
},
|
|
25495
|
+
run: async (cmd) => {
|
|
25496
|
+
const opts = cmd.opts();
|
|
25497
|
+
const cfg = requireConfig();
|
|
25498
|
+
const cred = requireAuth(loadCredentials());
|
|
25499
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
25500
|
+
const wantedFrom = opts.fromUid?.toUpperCase();
|
|
25501
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
25502
|
+
const { socket } = session;
|
|
25503
|
+
try {
|
|
25504
|
+
return await new Promise((resolve2, reject) => {
|
|
25505
|
+
const timer = setTimeout(() => {
|
|
25506
|
+
reject(new CliError(
|
|
25507
|
+
"TIMEOUT",
|
|
25508
|
+
`no matching reaction on mid=${opts.mid} after ${opts.timeout}s`
|
|
25509
|
+
));
|
|
25510
|
+
}, opts.timeout * 1e3);
|
|
25511
|
+
const cleanup = () => clearTimeout(timer);
|
|
25512
|
+
socket.on("kicked", (payload) => {
|
|
25513
|
+
cleanup();
|
|
25514
|
+
reject(new CliError(
|
|
25515
|
+
"KICKED",
|
|
25516
|
+
`chat session kicked: ${JSON.stringify(payload).slice(0, 200)}`,
|
|
25517
|
+
"another login claimed the same device slot \u2014 see HANDOFF.md slot model"
|
|
25518
|
+
));
|
|
25519
|
+
});
|
|
25520
|
+
socket.on("error", (e) => {
|
|
25521
|
+
cleanup();
|
|
25522
|
+
reject(new CliError("NETWORK", `chat socket error: ${e.message}`));
|
|
25523
|
+
});
|
|
25524
|
+
socket.on("close", () => {
|
|
25525
|
+
cleanup();
|
|
25526
|
+
reject(new CliError("NETWORK", "chat socket closed before a matching reaction arrived"));
|
|
25527
|
+
});
|
|
25528
|
+
socket.on("push", (frame) => {
|
|
25529
|
+
ackInboundIfNeeded(socket, frame);
|
|
25530
|
+
if (frame.sid !== "talk") return;
|
|
25531
|
+
if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
|
|
25532
|
+
const msgs = frame.data?.msgs ?? [];
|
|
25533
|
+
for (const m of msgs) {
|
|
25534
|
+
if (m?.type !== 3 || m?.subType !== 300) continue;
|
|
25535
|
+
const body = m.body ?? {};
|
|
25536
|
+
const refMid = String(body.mid ?? body.refMid ?? "");
|
|
25537
|
+
if (refMid !== opts.mid) continue;
|
|
25538
|
+
if (wantedFrom && String(m.fId ?? "").toUpperCase() !== wantedFrom) continue;
|
|
25539
|
+
if (opts.stickerId) {
|
|
25540
|
+
const sid = String(body.stickerId ?? "");
|
|
25541
|
+
if (sid !== opts.stickerId) continue;
|
|
25542
|
+
}
|
|
25543
|
+
cleanup();
|
|
25544
|
+
resolve2({ matched: true, mid: opts.mid, reaction: m });
|
|
25545
|
+
return;
|
|
25546
|
+
}
|
|
25547
|
+
});
|
|
25548
|
+
socket.send("sync", "syncDone", { timestamp: 0 }).catch(() => {
|
|
25549
|
+
});
|
|
25550
|
+
});
|
|
25551
|
+
} finally {
|
|
25552
|
+
socket.close();
|
|
25553
|
+
}
|
|
25554
|
+
}
|
|
25555
|
+
});
|
|
25556
|
+
}
|
|
25557
|
+
function ackInboundIfNeeded(socket, frame) {
|
|
25558
|
+
if (frame.sid !== "talk") return;
|
|
25559
|
+
if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
|
|
25560
|
+
const sqRaw = frame.sq;
|
|
25561
|
+
const sq = typeof sqRaw === "number" ? sqRaw : Number.parseInt(String(sqRaw ?? ""), 10);
|
|
25562
|
+
if (!Number.isFinite(sq)) return;
|
|
25563
|
+
const tids = Array.isArray(frame.data?.tids) ? frame.data.tids : [];
|
|
25564
|
+
if (tids.length === 0) return;
|
|
25565
|
+
try {
|
|
25566
|
+
socket.sendAck(frame.sid, frame.cid, sq, { tids });
|
|
25567
|
+
} catch {
|
|
25568
|
+
}
|
|
25569
|
+
}
|
|
25570
|
+
|
|
25571
|
+
// src/commands/listen.ts
|
|
25572
|
+
var MESSAGE_DATA_SCHEMA = {
|
|
25573
|
+
type: "object",
|
|
25574
|
+
properties: {
|
|
25575
|
+
mid: { type: "string" },
|
|
25576
|
+
sId: { type: "string" },
|
|
25577
|
+
fId: { type: "string" },
|
|
25578
|
+
fApp: { type: "string" },
|
|
25579
|
+
toId: { type: "string" },
|
|
25580
|
+
scene: { type: "string", enum: ["p2p", "team_g"] },
|
|
25581
|
+
type: { type: "number" },
|
|
25582
|
+
subType: { type: "number" },
|
|
25583
|
+
timestamp: { type: "number" },
|
|
25584
|
+
timestamp_u: { type: "number" },
|
|
25585
|
+
body: {}
|
|
25586
|
+
}
|
|
25587
|
+
};
|
|
25588
|
+
var STREAM_SCHEMA = {
|
|
25589
|
+
description: "NDJSON stream of envelopes. Under --json each stdout line is one JSON object: either {ok:true, schema_version, data:<message>} for a match, or {ok:false, schema_version, code, msg, hint?} for an in-band error (KICKED / NETWORK). Without --json each line is a one-line human summary (NOT JSON). Use --json for any pipe consumer.",
|
|
25590
|
+
oneOf: [
|
|
25591
|
+
{
|
|
25592
|
+
type: "object",
|
|
25593
|
+
required: ["ok", "schema_version", "data"],
|
|
25594
|
+
properties: {
|
|
25595
|
+
ok: { const: true },
|
|
25596
|
+
schema_version: { type: "number" },
|
|
25597
|
+
data: MESSAGE_DATA_SCHEMA
|
|
25598
|
+
}
|
|
25599
|
+
},
|
|
25600
|
+
{
|
|
25601
|
+
type: "object",
|
|
25602
|
+
required: ["ok", "schema_version", "code", "msg"],
|
|
25603
|
+
properties: {
|
|
25604
|
+
ok: { const: false },
|
|
25605
|
+
schema_version: { type: "number" },
|
|
25606
|
+
// The streaming `push` path emits KICKED / NETWORK in-band; the
|
|
25607
|
+
// wrapping `defineStream` action handler may also fail() with any
|
|
25608
|
+
// CliError code raised before the loop starts (USAGE for bad
|
|
25609
|
+
// --regex/--type, AUTH_REQUIRED/AUTH_EXPIRED from the credential
|
|
25610
|
+
// gate, CONFIG_MISSING/CONFIG_INVALID from `requireConfig`,
|
|
25611
|
+
// REMOTE_ERROR from the chat-login refresh path, INTERNAL for
|
|
25612
|
+
// unexpected throws). Advertise all of them.
|
|
25613
|
+
code: {
|
|
25614
|
+
type: "string",
|
|
25615
|
+
enum: [
|
|
25616
|
+
"KICKED",
|
|
25617
|
+
"NETWORK",
|
|
25618
|
+
"TIMEOUT",
|
|
25619
|
+
"USAGE",
|
|
25620
|
+
"AUTH_REQUIRED",
|
|
25621
|
+
"AUTH_EXPIRED",
|
|
25622
|
+
"NOT_FOUND",
|
|
25623
|
+
"REMOTE_ERROR",
|
|
25624
|
+
"CONFIG_MISSING",
|
|
25625
|
+
"CONFIG_INVALID",
|
|
25626
|
+
"INTERNAL"
|
|
25627
|
+
]
|
|
25628
|
+
},
|
|
25629
|
+
msg: { type: "string" },
|
|
25630
|
+
hint: { type: "string" }
|
|
25631
|
+
}
|
|
25632
|
+
}
|
|
25633
|
+
]
|
|
25634
|
+
};
|
|
25635
|
+
function register26(program3) {
|
|
25636
|
+
defineStream(program3, {
|
|
25637
|
+
name: "listen",
|
|
25638
|
+
description: "Tail incoming chat messages, filtered by sender / team / peer / keyword / regex / @-mention. Emit one line per match \u2014 NDJSON envelope under `--json`, human summary otherwise. Consumes a TCP slot.",
|
|
25639
|
+
streamSchema: STREAM_SCHEMA,
|
|
25640
|
+
configure: (cmd) => {
|
|
25641
|
+
cmd.option("--from-uid <uid>", "only emit messages from this UID (repeat for OR; matched against fId, case-insensitive)", collect, []).option("--team <id>", "only emit messages in this team (repeat for OR)", collect, []).option("--peer <uid>", "only emit p2p messages with this peer UID (repeat for OR)", collect, []).option("--contains <text>", "only emit messages whose text contains this substring, case-insensitive (repeat for OR)", collect, []).option("--regex <pattern>", "only emit messages whose text matches this RE2-flavoured regex (repeat for OR)", collect, []).option("--type <n>", "only emit messages of this `type` field (default 1; repeat for OR)", collectInt("--type"), []).option("--subtype <n>", "only emit messages of this `subType` field (repeat for OR; default = any)", collectInt("--subtype"), []).option("--max <n>", "exit after emitting this many matching lines", parsePosInt("--max")).option("--at-me", "only emit group messages that @-mention you (mirrors the desktop SDK isAtMe: scene=team_g, fId\u2260self, atIds includes self uid). Implies excluding self.").option("--include-self", "do not filter out messages whose fId is your own UID").option("--include-reactions", "also emit type=3 sub=300/301 (reaction add/remove) frames").option("--include-offline", "also emit messages flushed via cid=offMsg on connect (default: only post-connect msg)").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--debug", "forward wire-level debug events to stderr");
|
|
25642
|
+
},
|
|
25643
|
+
run: async (cmd) => {
|
|
25644
|
+
const opts = cmd.opts();
|
|
25645
|
+
const cred = requireAuth(loadCredentials());
|
|
25646
|
+
const wantedFromUids = new Set(opts.fromUid.map((s) => s.toUpperCase()));
|
|
25647
|
+
const wantedTeams = new Set(opts.team.map((s) => s));
|
|
25648
|
+
const wantedPeers = new Set(opts.peer.map((s) => s.toUpperCase()));
|
|
25649
|
+
const containsTerms = opts.contains.map((s) => s.toLowerCase());
|
|
25650
|
+
const regexes = opts.regex.map((p) => {
|
|
25651
|
+
try {
|
|
25652
|
+
return new RegExp(p);
|
|
25653
|
+
} catch (e) {
|
|
25654
|
+
throw new CliError("USAGE", `invalid --regex "${p}": ${e.message}`);
|
|
25655
|
+
}
|
|
25656
|
+
});
|
|
25657
|
+
const subtypeImpliesReactions = opts.subtype.some((s) => s === 300 || s === 301);
|
|
25658
|
+
const wantedTypes = opts.type.length ? new Set(opts.type) : /* @__PURE__ */ new Set([1, ...opts.includeReactions || subtypeImpliesReactions ? [3] : []]);
|
|
25659
|
+
const wantedSubtypes = opts.subtype.length ? new Set(opts.subtype) : null;
|
|
25660
|
+
const includeReactions = !!opts.includeReactions;
|
|
25661
|
+
const max = opts.max;
|
|
25662
|
+
const atMeOnly = !!opts.atMe;
|
|
25663
|
+
if (atMeOnly && opts.includeSelf) {
|
|
25664
|
+
throw new CliError("USAGE", "--at-me and --include-self are mutually exclusive (--at-me only matches messages from other users)");
|
|
25665
|
+
}
|
|
25666
|
+
const includeSelf = !!opts.includeSelf;
|
|
25667
|
+
const selfUidUpper = cred.uid.toUpperCase();
|
|
25668
|
+
const cfg = requireConfig();
|
|
25669
|
+
const selfAppKey = cfg.appKey;
|
|
25670
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
25671
|
+
const session = await openChatSession(cred, { os, debug: opts.debug });
|
|
25672
|
+
const { socket } = session;
|
|
25673
|
+
const json = isJsonMode();
|
|
25674
|
+
let matched = 0;
|
|
25675
|
+
let exited = false;
|
|
25676
|
+
const closeAndExit = (code) => {
|
|
25677
|
+
if (exited) return;
|
|
25678
|
+
exited = true;
|
|
25679
|
+
try {
|
|
25680
|
+
socket.close();
|
|
25681
|
+
} catch {
|
|
25682
|
+
}
|
|
25683
|
+
process.exit(code);
|
|
25684
|
+
};
|
|
25685
|
+
const onSigint = () => closeAndExit(0);
|
|
25686
|
+
process.on("SIGINT", onSigint);
|
|
25687
|
+
process.on("SIGTERM", onSigint);
|
|
25688
|
+
socket.on("kicked", (payload) => {
|
|
25689
|
+
emitErr(
|
|
25690
|
+
"KICKED",
|
|
25691
|
+
`chat session kicked: ${JSON.stringify(payload).slice(0, 200)}`,
|
|
25692
|
+
"another login claimed the same device slot \u2014 see HANDOFF.md slot model"
|
|
25693
|
+
);
|
|
25694
|
+
closeAndExit(5);
|
|
25695
|
+
});
|
|
25696
|
+
socket.on("error", (e) => {
|
|
25697
|
+
emitErr("NETWORK", `chat socket error: ${e.message}`);
|
|
25698
|
+
closeAndExit(5);
|
|
25699
|
+
});
|
|
25700
|
+
socket.on("close", () => {
|
|
25701
|
+
if (!exited) {
|
|
25702
|
+
emitErr("NETWORK", "chat socket closed unexpectedly");
|
|
25703
|
+
closeAndExit(5);
|
|
25704
|
+
}
|
|
25705
|
+
});
|
|
25706
|
+
socket.on("push", (frame) => {
|
|
25707
|
+
ackInboundIfNeeded2(socket, frame);
|
|
25708
|
+
if (frame.sid !== "talk") return;
|
|
25709
|
+
if (frame.cid !== "msg" && !(opts.includeOffline && frame.cid === "offMsg")) return;
|
|
25710
|
+
const msgs = frame.data?.msgs ?? [];
|
|
25711
|
+
for (const m of msgs) {
|
|
25712
|
+
if (!matches(m)) continue;
|
|
25713
|
+
emitMatch(m);
|
|
25714
|
+
matched++;
|
|
25715
|
+
if (max && matched >= max) {
|
|
25716
|
+
closeAndExit(0);
|
|
25717
|
+
return;
|
|
25718
|
+
}
|
|
25719
|
+
}
|
|
25720
|
+
});
|
|
25721
|
+
socket.send("sync", "syncDone", { timestamp: 0 }).catch(() => {
|
|
25722
|
+
});
|
|
25723
|
+
function matches(m) {
|
|
25724
|
+
if (!m || typeof m !== "object") return false;
|
|
25725
|
+
if (!wantedTypes.has(m.type)) {
|
|
25726
|
+
return false;
|
|
25727
|
+
}
|
|
25728
|
+
if (wantedSubtypes && !wantedSubtypes.has(m.subType)) return false;
|
|
25729
|
+
if (m.type === 3 && (m.subType === 300 || m.subType === 301) && !includeReactions && !opts.subtype.length && !opts.type.length) {
|
|
25730
|
+
return false;
|
|
25731
|
+
}
|
|
25732
|
+
if (!includeSelf && String(m.fId ?? "").toUpperCase() === selfUidUpper) {
|
|
25733
|
+
return false;
|
|
25734
|
+
}
|
|
25735
|
+
if (atMeOnly) {
|
|
25736
|
+
if (m.scene !== "team_g") return false;
|
|
25737
|
+
if (String(m.fId ?? "").toUpperCase() === selfUidUpper) return false;
|
|
25738
|
+
const atIds = Array.isArray(m.atIds) ? m.atIds : [];
|
|
25739
|
+
const idx = atIds.findIndex((id) => String(id ?? "").toUpperCase() === selfUidUpper);
|
|
25740
|
+
if (idx < 0) return false;
|
|
25741
|
+
const atAppkeys = Array.isArray(m.atAppkeys) ? m.atAppkeys : void 0;
|
|
25742
|
+
if (atAppkeys && selfAppKey) {
|
|
25743
|
+
const includesSelfApp = atAppkeys.includes(selfAppKey);
|
|
25744
|
+
const slotEmpty = !atAppkeys[idx];
|
|
25745
|
+
if (!includesSelfApp && !slotEmpty) return false;
|
|
25746
|
+
}
|
|
25747
|
+
}
|
|
25748
|
+
if (wantedFromUids.size && !wantedFromUids.has(String(m.fId ?? "").toUpperCase())) return false;
|
|
25749
|
+
if (wantedTeams.size) {
|
|
25750
|
+
if (m.scene !== "team_g") return false;
|
|
25751
|
+
if (!wantedTeams.has(String(m.toId ?? ""))) return false;
|
|
25752
|
+
}
|
|
25753
|
+
if (wantedPeers.size) {
|
|
25754
|
+
if (m.scene !== "p2p") return false;
|
|
25755
|
+
const fromId = String(m.fId ?? "").toUpperCase();
|
|
25756
|
+
const toId = String(m.toId ?? "").toUpperCase();
|
|
25757
|
+
const peer = fromId === cred.uid.toUpperCase() ? toId : fromId;
|
|
25758
|
+
if (!wantedPeers.has(peer)) return false;
|
|
25759
|
+
}
|
|
25760
|
+
if (containsTerms.length || regexes.length) {
|
|
25761
|
+
const text = extractText(m).toLowerCase();
|
|
25762
|
+
if (containsTerms.length && !containsTerms.some((t) => text.includes(t))) return false;
|
|
25763
|
+
if (regexes.length && !regexes.some((r) => r.test(text))) return false;
|
|
25764
|
+
}
|
|
25765
|
+
return true;
|
|
25766
|
+
}
|
|
25767
|
+
function emitMatch(m) {
|
|
25768
|
+
const out = {
|
|
25769
|
+
mid: String(m.mid ?? ""),
|
|
25770
|
+
sId: String(m.sId ?? ""),
|
|
25771
|
+
fId: String(m.fId ?? ""),
|
|
25772
|
+
fApp: String(m.fApp ?? ""),
|
|
25773
|
+
toId: String(m.toId ?? ""),
|
|
25774
|
+
scene: m.scene,
|
|
25775
|
+
type: m.type,
|
|
25776
|
+
subType: m.subType,
|
|
25777
|
+
timestamp: m.timestamp,
|
|
25778
|
+
timestamp_u: m.timestamp_u,
|
|
25779
|
+
body: m.body
|
|
25780
|
+
};
|
|
25781
|
+
if (json) {
|
|
25782
|
+
process.stdout.write(JSON.stringify({ ok: true, schema_version: SCHEMA_VERSION, data: out }) + "\n");
|
|
25783
|
+
} else {
|
|
25784
|
+
process.stdout.write(formatHuman(out) + "\n");
|
|
25785
|
+
}
|
|
25786
|
+
}
|
|
25787
|
+
function emitErr(code, msg, hint) {
|
|
25788
|
+
if (json) {
|
|
25789
|
+
process.stdout.write(JSON.stringify({ ok: false, schema_version: SCHEMA_VERSION, code, msg, hint }) + "\n");
|
|
25790
|
+
} else {
|
|
25791
|
+
process.stderr.write(`error: ${msg}
|
|
25792
|
+
`);
|
|
25793
|
+
if (hint) process.stderr.write(` hint: ${hint}
|
|
25794
|
+
`);
|
|
25795
|
+
process.stderr.write(` code: ${code}
|
|
25796
|
+
`);
|
|
25797
|
+
}
|
|
25798
|
+
}
|
|
25799
|
+
await new Promise(() => {
|
|
25800
|
+
});
|
|
25801
|
+
}
|
|
25802
|
+
});
|
|
25803
|
+
}
|
|
25804
|
+
function collect(value, prev) {
|
|
25805
|
+
return [...prev, value];
|
|
25806
|
+
}
|
|
25807
|
+
function collectInt(label) {
|
|
25808
|
+
return (raw, prev) => {
|
|
25809
|
+
if (!/^\d+$/.test(raw)) throw new CliError("USAGE", `${label} must be a non-negative integer (got ${raw})`);
|
|
25810
|
+
return [...prev, Number(raw)];
|
|
25811
|
+
};
|
|
25812
|
+
}
|
|
25813
|
+
function ackInboundIfNeeded2(socket, frame) {
|
|
25814
|
+
if (frame.sid !== "talk") return;
|
|
25815
|
+
if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
|
|
25816
|
+
const sqRaw = frame.sq;
|
|
25817
|
+
const sq = typeof sqRaw === "number" ? sqRaw : Number.parseInt(String(sqRaw ?? ""), 10);
|
|
25818
|
+
if (!Number.isFinite(sq)) return;
|
|
25819
|
+
const tids = Array.isArray(frame.data?.tids) ? frame.data.tids : [];
|
|
25820
|
+
if (tids.length === 0) return;
|
|
25821
|
+
try {
|
|
25822
|
+
socket.sendAck(frame.sid, frame.cid, sq, { tids });
|
|
25823
|
+
} catch {
|
|
25824
|
+
}
|
|
25825
|
+
}
|
|
25826
|
+
function extractText(m) {
|
|
25827
|
+
const body = m?.body;
|
|
25828
|
+
if (typeof body === "string") return body;
|
|
25829
|
+
if (Array.isArray(body)) {
|
|
25830
|
+
return body.map((seg) => typeof seg?.content === "string" ? seg.content : typeof seg?.text === "string" ? seg.text : "").join(" ");
|
|
25831
|
+
}
|
|
25832
|
+
if (body && typeof body === "object") {
|
|
25833
|
+
const candidates = ["text", "content", "fName", "name"];
|
|
25834
|
+
for (const k of candidates) {
|
|
25835
|
+
if (typeof body[k] === "string") return body[k];
|
|
25836
|
+
}
|
|
25837
|
+
}
|
|
25838
|
+
return "";
|
|
25839
|
+
}
|
|
25840
|
+
function formatHuman(m) {
|
|
25841
|
+
const ts = typeof m.timestamp === "number" ? new Date(m.timestamp * 1e3).toISOString() : "?";
|
|
25842
|
+
const where = m.scene === "team_g" ? `team:${m.toId}` : `p2p:${m.toId}`;
|
|
25843
|
+
const text = (() => {
|
|
25844
|
+
if (typeof m.body === "string") return m.body;
|
|
25845
|
+
if (m.body == null) return "";
|
|
25846
|
+
return JSON.stringify(m.body).slice(0, 200);
|
|
25847
|
+
})();
|
|
25848
|
+
return `${ts} ${m.fId} \u2192 ${where} (type=${m.type}/${m.subType}): ${text}`;
|
|
25849
|
+
}
|
|
25850
|
+
|
|
25851
|
+
// src/commands/manifest.ts
|
|
25852
|
+
var Output26 = external_exports.object({
|
|
25096
25853
|
version: external_exports.string(),
|
|
25097
25854
|
schema_version: external_exports.literal(1),
|
|
25098
25855
|
commands: external_exports.array(external_exports.object({
|
|
@@ -25101,11 +25858,11 @@ var Output22 = external_exports.object({
|
|
|
25101
25858
|
output_schema: external_exports.unknown()
|
|
25102
25859
|
}))
|
|
25103
25860
|
});
|
|
25104
|
-
function
|
|
25861
|
+
function register27(program3) {
|
|
25105
25862
|
define2(program3, {
|
|
25106
25863
|
name: "manifest",
|
|
25107
25864
|
description: "Emit the full command graph + per-command output JSON Schema. Skill authors should consume this rather than parse --help.",
|
|
25108
|
-
output:
|
|
25865
|
+
output: Output26,
|
|
25109
25866
|
run: async () => ({
|
|
25110
25867
|
version: CLI_VERSION,
|
|
25111
25868
|
schema_version: 1,
|
|
@@ -25152,6 +25909,11 @@ register19(program2);
|
|
|
25152
25909
|
register20(program2);
|
|
25153
25910
|
register21(program2);
|
|
25154
25911
|
register22(program2);
|
|
25912
|
+
register23(program2);
|
|
25913
|
+
register24(program2);
|
|
25914
|
+
register25(program2);
|
|
25915
|
+
register26(program2);
|
|
25916
|
+
register27(program2);
|
|
25155
25917
|
async function main() {
|
|
25156
25918
|
try {
|
|
25157
25919
|
await program2.parseAsync(argv);
|