zalo-agent-cli 1.0.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/DISCLAIMER.md +13 -0
- package/LICENSE +21 -0
- package/README.md +461 -0
- package/package.json +47 -0
- package/src/cli.test.js +97 -0
- package/src/commands/account.js +178 -0
- package/src/commands/conv.js +93 -0
- package/src/commands/friend.js +148 -0
- package/src/commands/group.js +123 -0
- package/src/commands/login.js +154 -0
- package/src/commands/msg.js +225 -0
- package/src/core/accounts.js +93 -0
- package/src/core/credentials.js +63 -0
- package/src/core/zalo-client.js +128 -0
- package/src/index.js +45 -0
- package/src/utils/bank-helpers.js +233 -0
- package/src/utils/bank-helpers.test.js +66 -0
- package/src/utils/output.js +21 -0
- package/src/utils/proxy-helpers.js +14 -0
- package/src/utils/proxy-helpers.test.js +36 -0
- package/src/utils/qr-display.js +78 -0
- package/src/utils/qr-http-server.js +126 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account commands — multi-account management with per-account proxy.
|
|
3
|
+
* Includes export for headless/CI credential transfer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, chmodSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import { loginWithQR, loginWithCredentials, extractCredentials, clearSession } from "../core/zalo-client.js";
|
|
9
|
+
import { saveCredentials, loadCredentials } from "../core/credentials.js";
|
|
10
|
+
import { listAccounts, getActive, setActive, addAccount, removeAccount, getAccount } from "../core/accounts.js";
|
|
11
|
+
import { maskProxy } from "../utils/proxy-helpers.js";
|
|
12
|
+
import { displayQR, getQRPath } from "../utils/qr-display.js";
|
|
13
|
+
import { startQrServer } from "../utils/qr-http-server.js";
|
|
14
|
+
import { success, error, info, warning, output } from "../utils/output.js";
|
|
15
|
+
|
|
16
|
+
export function registerAccountCommands(program) {
|
|
17
|
+
const account = program.command("account").description("Manage multiple Zalo accounts with proxy");
|
|
18
|
+
|
|
19
|
+
account
|
|
20
|
+
.command("list")
|
|
21
|
+
.description("List all registered accounts")
|
|
22
|
+
.action(() => {
|
|
23
|
+
const accounts = listAccounts();
|
|
24
|
+
const safe = accounts.map((a) => ({ ...a, proxy: maskProxy(a.proxy) }));
|
|
25
|
+
output(safe, program.opts().json, () => {
|
|
26
|
+
if (!accounts.length) {
|
|
27
|
+
info("No accounts. Use: zalo-agent account login");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(` ${"Active".padEnd(8)} ${"Owner ID".padEnd(22)} ${"Name".padEnd(20)} Proxy`);
|
|
31
|
+
console.log(` ${"─".repeat(75)}`);
|
|
32
|
+
for (const a of accounts) {
|
|
33
|
+
const marker = a.active ? " ★" : " ";
|
|
34
|
+
console.log(
|
|
35
|
+
` ${marker.padEnd(8)} ${a.ownId.padEnd(22)} ${(a.name || "").padEnd(20)} ${maskProxy(a.proxy)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
account
|
|
42
|
+
.command("login")
|
|
43
|
+
.description("Login a new Zalo account via QR code")
|
|
44
|
+
.option("-p, --proxy <url>", "Dedicated proxy URL for this account")
|
|
45
|
+
.option("-n, --name <label>", "Friendly label", "")
|
|
46
|
+
.option("--qr-url", "Start local HTTP server to view QR in browser (for VPS/headless)")
|
|
47
|
+
.action(async (opts) => {
|
|
48
|
+
if (opts.proxy) info(`Using proxy: ${maskProxy(opts.proxy)}`);
|
|
49
|
+
info("Generating QR code... Scan with Zalo mobile app.");
|
|
50
|
+
|
|
51
|
+
let qrServer = null;
|
|
52
|
+
try {
|
|
53
|
+
const { ownId } = await loginWithQR(opts.proxy, (event) => {
|
|
54
|
+
displayQR(event);
|
|
55
|
+
if (!qrServer) {
|
|
56
|
+
qrServer = startQrServer(getQRPath());
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Fetch display name from Zalo profile
|
|
61
|
+
let displayName = opts.name || "";
|
|
62
|
+
try {
|
|
63
|
+
const { getApi } = await import("../core/zalo-client.js");
|
|
64
|
+
const accountInfo = await getApi().fetchAccountInfo();
|
|
65
|
+
displayName = accountInfo?.profile?.displayName || displayName || ownId;
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
const creds = extractCredentials();
|
|
69
|
+
saveCredentials(ownId, creds);
|
|
70
|
+
addAccount(ownId, displayName, opts.proxy);
|
|
71
|
+
success(
|
|
72
|
+
`Account logged in: ${displayName} (${ownId})${opts.proxy ? ` via ${maskProxy(opts.proxy)}` : ""}`,
|
|
73
|
+
);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error(`Login failed: ${e.message}`);
|
|
76
|
+
} finally {
|
|
77
|
+
if (qrServer) qrServer.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
account
|
|
82
|
+
.command("switch <ownerId>")
|
|
83
|
+
.description("Switch active account (restarts connection with account proxy)")
|
|
84
|
+
.action(async (ownerId) => {
|
|
85
|
+
let acc = getAccount(ownerId);
|
|
86
|
+
if (!acc) {
|
|
87
|
+
const all = listAccounts();
|
|
88
|
+
const matches = all.filter((a) => a.ownId.includes(ownerId) || (a.name || "").includes(ownerId));
|
|
89
|
+
if (matches.length === 1) {
|
|
90
|
+
ownerId = matches[0].ownId;
|
|
91
|
+
acc = matches[0];
|
|
92
|
+
} else {
|
|
93
|
+
error(`Account not found: ${ownerId}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const creds = loadCredentials(ownerId);
|
|
99
|
+
if (!creds) {
|
|
100
|
+
error(`No credentials for ${ownerId}. Re-login needed.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
info(`Switching to ${ownerId} (${acc?.name || ""})`);
|
|
105
|
+
if (acc?.proxy) info(`Proxy: ${maskProxy(acc.proxy)}`);
|
|
106
|
+
|
|
107
|
+
clearSession();
|
|
108
|
+
try {
|
|
109
|
+
await loginWithCredentials(creds, acc?.proxy || null);
|
|
110
|
+
setActive(ownerId);
|
|
111
|
+
success(`Switched to ${ownerId}`);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
error(`Switch failed: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
account
|
|
118
|
+
.command("remove <ownerId>")
|
|
119
|
+
.description("Remove account and delete its credentials")
|
|
120
|
+
.action((ownerId) => {
|
|
121
|
+
if (removeAccount(ownerId)) {
|
|
122
|
+
success(`Account ${ownerId} removed`);
|
|
123
|
+
} else {
|
|
124
|
+
error(`Account not found: ${ownerId}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
account
|
|
129
|
+
.command("info")
|
|
130
|
+
.description("Show currently active account")
|
|
131
|
+
.action(() => {
|
|
132
|
+
const active = getActive();
|
|
133
|
+
const safe = active ? { ...active, proxy: maskProxy(active.proxy) } : null;
|
|
134
|
+
output(safe, program.opts().json, () => {
|
|
135
|
+
if (!active) {
|
|
136
|
+
info("No active account.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(` Owner ID: ${active.ownId}`);
|
|
140
|
+
console.log(` Name: ${active.name || "-"}`);
|
|
141
|
+
console.log(` Proxy: ${maskProxy(active.proxy)}`);
|
|
142
|
+
console.log(` Active: yes`);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
account
|
|
147
|
+
.command("export [ownerId]")
|
|
148
|
+
.description("Export account credentials for transfer to another machine")
|
|
149
|
+
.option("-o, --output <path>", "Output file path", "./zalo-creds.json")
|
|
150
|
+
.action((ownerId, opts) => {
|
|
151
|
+
const acc = ownerId ? getAccount(ownerId) : getActive();
|
|
152
|
+
if (!acc) {
|
|
153
|
+
error("No account found to export.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const creds = loadCredentials(acc.ownId);
|
|
158
|
+
if (!creds) {
|
|
159
|
+
error(`No credentials for ${acc.ownId}.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exportData = {
|
|
164
|
+
...creds,
|
|
165
|
+
proxy: acc.proxy || null,
|
|
166
|
+
ownId: acc.ownId,
|
|
167
|
+
name: acc.name || "",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const outPath = resolve(opts.output);
|
|
171
|
+
writeFileSync(outPath, JSON.stringify(exportData, null, 2), "utf-8");
|
|
172
|
+
chmodSync(outPath, 0o600);
|
|
173
|
+
|
|
174
|
+
success(`Exported to ${outPath}`);
|
|
175
|
+
warning("This file contains login credentials. Keep it secure and do not commit to git.");
|
|
176
|
+
info(`Import on another machine: zalo-agent login --credentials ${opts.output}`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation commands — pinned, archived, mute, unmute, read, unread, delete.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerConvCommands(program) {
|
|
9
|
+
const conv = program.command("conv").description("Manage conversations");
|
|
10
|
+
|
|
11
|
+
conv.command("pinned")
|
|
12
|
+
.description("List pinned conversations")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getApi().getPinnedConversations();
|
|
16
|
+
output(result, program.opts().json);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error(e.message);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
conv.command("archived")
|
|
23
|
+
.description("List archived conversations")
|
|
24
|
+
.action(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await getApi().getArchivedConversations();
|
|
27
|
+
output(result, program.opts().json);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
error(e.message);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
conv.command("mute <threadId>")
|
|
34
|
+
.description("Mute a conversation")
|
|
35
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
36
|
+
.option("-d, --duration <secs>", "Duration in seconds (-1 = forever)", "-1")
|
|
37
|
+
.action(async (threadId, opts) => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await getApi().setMute(threadId, Number(opts.type), Number(opts.duration));
|
|
40
|
+
output(result, program.opts().json, () => success("Conversation muted"));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
error(e.message);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
conv.command("unmute <threadId>")
|
|
47
|
+
.description("Unmute a conversation")
|
|
48
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
49
|
+
.action(async (threadId, opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const result = await getApi().setMute(threadId, Number(opts.type), 0);
|
|
52
|
+
output(result, program.opts().json, () => success("Conversation unmuted"));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
error(e.message);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
conv.command("read <threadId>")
|
|
59
|
+
.description("Mark conversation as read")
|
|
60
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
61
|
+
.action(async (threadId, opts) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await getApi().sendSeenEvent(threadId, Number(opts.type));
|
|
64
|
+
output(result, program.opts().json, () => success("Marked as read"));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
error(e.message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
conv.command("unread <threadId>")
|
|
71
|
+
.description("Mark conversation as unread")
|
|
72
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
73
|
+
.action(async (threadId, opts) => {
|
|
74
|
+
try {
|
|
75
|
+
const result = await getApi().markAsUnread(threadId, Number(opts.type));
|
|
76
|
+
output(result, program.opts().json, () => success("Marked as unread"));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
error(e.message);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
conv.command("delete <threadId>")
|
|
83
|
+
.description("Delete conversation history")
|
|
84
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
85
|
+
.action(async (threadId, opts) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await getApi().deleteConversation(threadId, Number(opts.type));
|
|
88
|
+
output(result, program.opts().json, () => success("Conversation deleted"));
|
|
89
|
+
} catch (e) {
|
|
90
|
+
error(e.message);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friend commands — list, find, info, add, accept, remove, block, unblock, last-online, online.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerFriendCommands(program) {
|
|
9
|
+
const friend = program.command("friend").description("Manage friends and contacts");
|
|
10
|
+
|
|
11
|
+
friend
|
|
12
|
+
.command("list")
|
|
13
|
+
.description("List all friends")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await getApi().getAllFriends();
|
|
17
|
+
output(result, program.opts().json, () => {
|
|
18
|
+
const profiles = result?.changed_profiles || result || {};
|
|
19
|
+
const entries = Object.entries(profiles);
|
|
20
|
+
info(`${entries.length} friends`);
|
|
21
|
+
for (const [uid, p] of entries) {
|
|
22
|
+
console.log(` ${uid} ${p.displayName || p.zaloName || "?"}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
error(e.message);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
friend
|
|
31
|
+
.command("online")
|
|
32
|
+
.description("List currently online friends")
|
|
33
|
+
.action(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await getApi().getFriendOnlines();
|
|
36
|
+
output(result, program.opts().json);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
error(e.message);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
friend
|
|
43
|
+
.command("find <query>")
|
|
44
|
+
.description("Find user by phone number or Zalo ID")
|
|
45
|
+
.action(async (query) => {
|
|
46
|
+
try {
|
|
47
|
+
const result = await getApi().findUser(query);
|
|
48
|
+
output(result, program.opts().json, () => {
|
|
49
|
+
const u = result?.uid ? result : result?.data || result;
|
|
50
|
+
info(`User ID: ${u.uid || "?"}`);
|
|
51
|
+
info(`Name: ${u.displayName || u.zaloName || "?"}`);
|
|
52
|
+
});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
error(e.message);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
friend
|
|
59
|
+
.command("info <userId>")
|
|
60
|
+
.description("Get user profile information")
|
|
61
|
+
.action(async (userId) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await getApi().getUserInfo(userId);
|
|
64
|
+
output(result, program.opts().json, () => {
|
|
65
|
+
const profiles = result?.changed_profiles || {};
|
|
66
|
+
const p = profiles[userId] || {};
|
|
67
|
+
info(`Name: ${p.displayName || p.zaloName || "?"}`);
|
|
68
|
+
info(`Phone: ${p.phoneNumber || "?"}`);
|
|
69
|
+
info(`Avatar: ${p.avatar || "?"}`);
|
|
70
|
+
});
|
|
71
|
+
} catch (e) {
|
|
72
|
+
error(e.message);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
friend
|
|
77
|
+
.command("add <userId>")
|
|
78
|
+
.description("Send a friend request")
|
|
79
|
+
.option("-m, --msg <text>", "Message to include", "")
|
|
80
|
+
.action(async (userId, opts) => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await getApi().sendFriendRequest(userId, opts.msg);
|
|
83
|
+
output(result, program.opts().json, () => success("Friend request sent"));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
error(e.message);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
friend
|
|
90
|
+
.command("accept <userId>")
|
|
91
|
+
.description("Accept a friend request")
|
|
92
|
+
.action(async (userId) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await getApi().acceptFriendRequest(userId);
|
|
95
|
+
output(result, program.opts().json, () => success("Friend request accepted"));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
error(e.message);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
friend
|
|
102
|
+
.command("remove <userId>")
|
|
103
|
+
.description("Remove a friend")
|
|
104
|
+
.action(async (userId) => {
|
|
105
|
+
try {
|
|
106
|
+
const result = await getApi().removeFriend(userId);
|
|
107
|
+
output(result, program.opts().json, () => success("Friend removed"));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
error(e.message);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
friend
|
|
114
|
+
.command("block <userId>")
|
|
115
|
+
.description("Block a user")
|
|
116
|
+
.action(async (userId) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await getApi().blockUser(userId);
|
|
119
|
+
output(result, program.opts().json, () => success("User blocked"));
|
|
120
|
+
} catch (e) {
|
|
121
|
+
error(e.message);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
friend
|
|
126
|
+
.command("unblock <userId>")
|
|
127
|
+
.description("Unblock a user")
|
|
128
|
+
.action(async (userId) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await getApi().unblockUser(userId);
|
|
131
|
+
output(result, program.opts().json, () => success("User unblocked"));
|
|
132
|
+
} catch (e) {
|
|
133
|
+
error(e.message);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
friend
|
|
138
|
+
.command("last-online <userId>")
|
|
139
|
+
.description("Check when user was last online")
|
|
140
|
+
.action(async (userId) => {
|
|
141
|
+
try {
|
|
142
|
+
const result = await getApi().getLastOnline(userId);
|
|
143
|
+
output(result, program.opts().json);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
error(e.message);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group commands — list, create, info, members, add/remove-member, rename, leave, join.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerGroupCommands(program) {
|
|
9
|
+
const group = program.command("group").description("Manage groups");
|
|
10
|
+
|
|
11
|
+
group
|
|
12
|
+
.command("list")
|
|
13
|
+
.description("List all groups")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await getApi().getAllGroups();
|
|
17
|
+
output(result, program.opts().json);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
error(e.message);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
group
|
|
24
|
+
.command("create <name> <memberIds...>")
|
|
25
|
+
.description("Create a new group")
|
|
26
|
+
.action(async (name, memberIds) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await getApi().createGroup({ members: memberIds, name });
|
|
29
|
+
output(result, program.opts().json, () => success(`Group "${name}" created`));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
error(e.message);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
group
|
|
36
|
+
.command("info <groupId>")
|
|
37
|
+
.description("Show group details")
|
|
38
|
+
.action(async (groupId) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await getApi().getGroupInfo(groupId);
|
|
41
|
+
output(result, program.opts().json);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
error(e.message);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
group
|
|
48
|
+
.command("members <groupId>")
|
|
49
|
+
.description("List group members")
|
|
50
|
+
.action(async (groupId) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await getApi().getGroupInfo(groupId);
|
|
53
|
+
const members = result?.gridInfoMap?.[groupId]?.memberIds || {};
|
|
54
|
+
output(members, program.opts().json, () => {
|
|
55
|
+
const ids = Object.keys(members);
|
|
56
|
+
info(`${ids.length} members`);
|
|
57
|
+
ids.forEach((id) => console.log(` ${id}`));
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
error(e.message);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
group
|
|
65
|
+
.command("add-member <groupId> <userIds...>")
|
|
66
|
+
.description("Add members to a group")
|
|
67
|
+
.action(async (groupId, userIds) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await getApi().addUserToGroup(userIds, groupId);
|
|
70
|
+
output(result, program.opts().json, () => success("Member(s) added"));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
error(e.message);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
group
|
|
77
|
+
.command("remove-member <groupId> <userIds...>")
|
|
78
|
+
.description("Remove members from a group")
|
|
79
|
+
.action(async (groupId, userIds) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await getApi().removeUserFromGroup(userIds, groupId);
|
|
82
|
+
output(result, program.opts().json, () => success("Member(s) removed"));
|
|
83
|
+
} catch (e) {
|
|
84
|
+
error(e.message);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
group
|
|
89
|
+
.command("rename <groupId> <name>")
|
|
90
|
+
.description("Rename a group")
|
|
91
|
+
.action(async (groupId, name) => {
|
|
92
|
+
try {
|
|
93
|
+
const result = await getApi().changeGroupName(groupId, name);
|
|
94
|
+
output(result, program.opts().json, () => success(`Group renamed to "${name}"`));
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error(e.message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
group
|
|
101
|
+
.command("leave <groupId>")
|
|
102
|
+
.description("Leave a group")
|
|
103
|
+
.action(async (groupId) => {
|
|
104
|
+
try {
|
|
105
|
+
const result = await getApi().leaveGroup(groupId);
|
|
106
|
+
output(result, program.opts().json, () => success("Left group"));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
error(e.message);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
group
|
|
113
|
+
.command("join <link>")
|
|
114
|
+
.description("Join a group via invite link")
|
|
115
|
+
.action(async (link) => {
|
|
116
|
+
try {
|
|
117
|
+
const result = await getApi().joinGroup(link);
|
|
118
|
+
output(result, program.opts().json, () => success("Joined group"));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
error(e.message);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login commands — QR login, credential login, logout, status, whoami.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
6
|
+
import {
|
|
7
|
+
loginWithQR,
|
|
8
|
+
loginWithCredentials,
|
|
9
|
+
extractCredentials,
|
|
10
|
+
clearSession,
|
|
11
|
+
isLoggedIn,
|
|
12
|
+
getApi,
|
|
13
|
+
getOwnId,
|
|
14
|
+
} from "../core/zalo-client.js";
|
|
15
|
+
import { saveCredentials, deleteCredentials } from "../core/credentials.js";
|
|
16
|
+
import { addAccount, getActive, removeAccount } from "../core/accounts.js";
|
|
17
|
+
import { maskProxy } from "../utils/proxy-helpers.js";
|
|
18
|
+
import { displayQR, getQRPath } from "../utils/qr-display.js";
|
|
19
|
+
import { startQrServer } from "../utils/qr-http-server.js";
|
|
20
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
21
|
+
|
|
22
|
+
export function registerLoginCommands(program) {
|
|
23
|
+
program
|
|
24
|
+
.command("login")
|
|
25
|
+
.description("Login to Zalo via QR code scan or from exported credentials")
|
|
26
|
+
.option("-p, --proxy <url>", "Proxy URL (http/https/socks5://[user:pass@]host:port)")
|
|
27
|
+
.option("-n, --name <label>", "Friendly name for this account", "")
|
|
28
|
+
.option("--qr-url", "Start local HTTP server to view QR in browser (for VPS/headless)")
|
|
29
|
+
.option("--credentials <path>", "Login from exported credentials file (skip QR)")
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
// Credential-based login (headless/CI)
|
|
32
|
+
if (opts.credentials) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(opts.credentials, "utf-8"));
|
|
35
|
+
const proxy = opts.proxy || raw.proxy || null;
|
|
36
|
+
if (proxy) info(`Using proxy: ${maskProxy(proxy)}`);
|
|
37
|
+
|
|
38
|
+
const { ownId } = await loginWithCredentials(raw, proxy);
|
|
39
|
+
|
|
40
|
+
let displayName = opts.name || raw.name || "";
|
|
41
|
+
try {
|
|
42
|
+
const accountInfo = await getApi().fetchAccountInfo();
|
|
43
|
+
displayName = accountInfo?.profile?.displayName || displayName || ownId;
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
const creds = extractCredentials();
|
|
47
|
+
saveCredentials(ownId, creds);
|
|
48
|
+
addAccount(ownId, displayName, proxy);
|
|
49
|
+
success(`Logged in as ${displayName} (${ownId})`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
error(`Login from credentials failed: ${e.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// QR-based login
|
|
58
|
+
if (opts.proxy) info(`Using proxy: ${maskProxy(opts.proxy)}`);
|
|
59
|
+
info("Generating QR code... Scan with Zalo mobile app.");
|
|
60
|
+
|
|
61
|
+
let qrServer = null;
|
|
62
|
+
try {
|
|
63
|
+
const { ownId } = await loginWithQR(opts.proxy, (event) => {
|
|
64
|
+
displayQR(event);
|
|
65
|
+
|
|
66
|
+
// Always start HTTP server for QR scanning (no flag needed)
|
|
67
|
+
if (!qrServer) {
|
|
68
|
+
qrServer = startQrServer(getQRPath());
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Fetch display name from Zalo profile
|
|
73
|
+
let displayName = opts.name || "";
|
|
74
|
+
try {
|
|
75
|
+
const accountInfo = await getApi().fetchAccountInfo();
|
|
76
|
+
displayName = accountInfo?.profile?.displayName || displayName || ownId;
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
const creds = extractCredentials();
|
|
80
|
+
saveCredentials(ownId, creds);
|
|
81
|
+
addAccount(ownId, displayName, opts.proxy);
|
|
82
|
+
success(`Logged in as ${displayName} (${ownId})`);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
error(`Login failed: ${e.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
} finally {
|
|
87
|
+
if (qrServer) qrServer.close();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command("logout")
|
|
93
|
+
.description("Logout current account (keeps credentials for auto-login)")
|
|
94
|
+
.option("--purge", "Also delete saved credentials (must QR login again)")
|
|
95
|
+
.action((opts) => {
|
|
96
|
+
const active = getActive();
|
|
97
|
+
clearSession();
|
|
98
|
+
|
|
99
|
+
if (opts.purge && active) {
|
|
100
|
+
deleteCredentials(active.ownId);
|
|
101
|
+
removeAccount(active.ownId);
|
|
102
|
+
// Also remove QR image
|
|
103
|
+
try {
|
|
104
|
+
unlinkSync(getQRPath());
|
|
105
|
+
} catch {}
|
|
106
|
+
success(`Logged out and purged credentials for ${active.name || active.ownId}`);
|
|
107
|
+
} else {
|
|
108
|
+
success("Logged out (credentials kept — will auto-login on next command)");
|
|
109
|
+
if (active) info(`To fully remove: zalo-agent account remove ${active.ownId}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command("status")
|
|
115
|
+
.description("Show current login status")
|
|
116
|
+
.action(() => {
|
|
117
|
+
const active = getActive();
|
|
118
|
+
const data = {
|
|
119
|
+
loggedIn: isLoggedIn(),
|
|
120
|
+
ownId: getOwnId(),
|
|
121
|
+
activeAccount: active
|
|
122
|
+
? { ownId: active.ownId, name: active.name, proxy: maskProxy(active.proxy) }
|
|
123
|
+
: null,
|
|
124
|
+
};
|
|
125
|
+
output(data, program.opts().json, () => {
|
|
126
|
+
if (data.loggedIn) {
|
|
127
|
+
success(`Logged in as ${data.ownId}`);
|
|
128
|
+
if (active) info(`Account: ${active.name || active.ownId} | Proxy: ${maskProxy(active.proxy)}`);
|
|
129
|
+
} else {
|
|
130
|
+
info("Not logged in");
|
|
131
|
+
if (active)
|
|
132
|
+
info(`Active account: ${active.name || active.ownId} (will auto-login on next command)`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command("whoami")
|
|
139
|
+
.description("Show current user profile")
|
|
140
|
+
.action(async () => {
|
|
141
|
+
try {
|
|
142
|
+
const api = getApi();
|
|
143
|
+
const accountInfo = await api.fetchAccountInfo();
|
|
144
|
+
output(accountInfo, program.opts().json, () => {
|
|
145
|
+
const p = accountInfo?.profile || {};
|
|
146
|
+
info(`Name: ${p.displayName || "?"}`);
|
|
147
|
+
info(`ID: ${p.userId || getOwnId()}`);
|
|
148
|
+
info(`Phone: ${p.phoneNumber || "?"}`);
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
error(e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|