@gakr-gakr/zalouser 0.1.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.
Files changed (51) hide show
  1. package/README.md +120 -0
  2. package/api.ts +9 -0
  3. package/autobot.plugin.json +18 -0
  4. package/channel-plugin-api.ts +3 -0
  5. package/contract-api.ts +2 -0
  6. package/doctor-contract-api.ts +1 -0
  7. package/index.ts +34 -0
  8. package/package.json +68 -0
  9. package/runtime-api.ts +62 -0
  10. package/secret-contract-api.ts +4 -0
  11. package/setup-entry.ts +9 -0
  12. package/setup-plugin-api.ts +2 -0
  13. package/src/accounts.runtime.ts +1 -0
  14. package/src/accounts.test-mocks.ts +14 -0
  15. package/src/accounts.ts +136 -0
  16. package/src/channel-api.ts +20 -0
  17. package/src/channel.adapters.ts +432 -0
  18. package/src/channel.runtime.ts +12 -0
  19. package/src/channel.setup.ts +12 -0
  20. package/src/channel.ts +221 -0
  21. package/src/config-schema.ts +33 -0
  22. package/src/directory.ts +54 -0
  23. package/src/doctor-contract.ts +156 -0
  24. package/src/doctor.ts +37 -0
  25. package/src/group-policy.ts +83 -0
  26. package/src/message-sid.ts +80 -0
  27. package/src/monitor.send-mocks.ts +20 -0
  28. package/src/monitor.ts +1057 -0
  29. package/src/probe.ts +35 -0
  30. package/src/qr-temp-file.ts +22 -0
  31. package/src/reaction.ts +32 -0
  32. package/src/runtime.ts +9 -0
  33. package/src/security-audit.ts +71 -0
  34. package/src/send-receipt.ts +31 -0
  35. package/src/send.ts +280 -0
  36. package/src/session-route.ts +121 -0
  37. package/src/setup-core.ts +36 -0
  38. package/src/setup-surface.ts +485 -0
  39. package/src/setup-test-helpers.ts +42 -0
  40. package/src/shared.ts +92 -0
  41. package/src/status-issues.ts +58 -0
  42. package/src/text-styles.ts +540 -0
  43. package/src/tool.ts +200 -0
  44. package/src/types.ts +127 -0
  45. package/src/zalo-js.test-mocks.ts +89 -0
  46. package/src/zalo-js.ts +1889 -0
  47. package/src/zca-client.ts +259 -0
  48. package/src/zca-constants.ts +55 -0
  49. package/src/zca-js-exports.d.ts +22 -0
  50. package/test-api.ts +21 -0
  51. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @gakr-gakr/zalouser
2
+
3
+ AutoBot extension for Zalo Personal Account messaging via native `zca-js` integration.
4
+
5
+ > **Warning:** Using Zalo automation may result in account suspension or ban. Use at your own risk. This is an unofficial integration.
6
+
7
+ ## Features
8
+
9
+ - Channel plugin integration with setup wizard + QR login
10
+ - In-process listener/sender via `zca-js` (no external CLI)
11
+ - Multi-account support
12
+ - Agent tool integration (`zalouser`)
13
+ - DM/group policy support
14
+
15
+ ## Prerequisites
16
+
17
+ - AutoBot Gateway
18
+ - Zalo mobile app (for QR login)
19
+
20
+ No external `zca`, `openzca`, or `zca-cli` binary is required.
21
+
22
+ ## Install
23
+
24
+ ### Option A: npm
25
+
26
+ ```bash
27
+ autobot plugins install @gakr-gakr/zalouser
28
+ ```
29
+
30
+ ### Option B: local source checkout
31
+
32
+ ```bash
33
+ PLUGIN_SRC=./path/to/local/zalouser-plugin
34
+ autobot plugins install "$PLUGIN_SRC"
35
+ cd "$PLUGIN_SRC" && pnpm install
36
+ ```
37
+
38
+ Restart the Gateway after install.
39
+
40
+ ## Quick start
41
+
42
+ ### Login (QR)
43
+
44
+ ```bash
45
+ autobot channels login --channel zalouser
46
+ ```
47
+
48
+ Scan the QR code with the Zalo app on your phone.
49
+
50
+ ### Enable channel
51
+
52
+ ```yaml
53
+ channels:
54
+ zalouser:
55
+ enabled: true
56
+ dmPolicy: pairing # pairing | allowlist | open | disabled
57
+ ```
58
+
59
+ ### Send a message
60
+
61
+ ```bash
62
+ autobot message send --channel zalouser --target <threadId> --message "Hello from AutoBot"
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ Basic:
68
+
69
+ ```yaml
70
+ channels:
71
+ zalouser:
72
+ enabled: true
73
+ dmPolicy: pairing
74
+ ```
75
+
76
+ Multi-account:
77
+
78
+ ```yaml
79
+ channels:
80
+ zalouser:
81
+ enabled: true
82
+ defaultAccount: default
83
+ accounts:
84
+ default:
85
+ enabled: true
86
+ profile: default
87
+ work:
88
+ enabled: true
89
+ profile: work
90
+ ```
91
+
92
+ ## Useful commands
93
+
94
+ ```bash
95
+ autobot channels login --channel zalouser
96
+ autobot channels login --channel zalouser --account work
97
+ autobot channels status --probe
98
+ autobot channels logout --channel zalouser
99
+
100
+ autobot directory self --channel zalouser
101
+ autobot directory peers list --channel zalouser --query "name"
102
+ autobot directory groups list --channel zalouser --query "work"
103
+ autobot directory groups members --channel zalouser --group-id <id>
104
+ ```
105
+
106
+ ## Agent tool
107
+
108
+ The extension registers a `zalouser` tool for AI agents.
109
+
110
+ Available actions: `send`, `image`, `link`, `friends`, `groups`, `me`, `status`
111
+
112
+ ## Troubleshooting
113
+
114
+ - Login not persisted: `autobot channels logout --channel zalouser && autobot channels login --channel zalouser`
115
+ - Probe status: `autobot channels status --probe`
116
+ - Name resolution issues (allowlist/groups): use numeric IDs or exact Zalo names
117
+
118
+ ## Credits
119
+
120
+ Built on [zca-js](https://github.com/RFS-ADRENO/zca-js).
package/api.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { zalouserPlugin } from "./src/channel.js";
2
+ export { zalouserSetupPlugin } from "./src/channel.setup.js";
3
+ export { createZalouserTool } from "./src/tool.js";
4
+ export { createZalouserSetupWizardProxy, zalouserSetupAdapter } from "./src/setup-core.js";
5
+ export { zalouserSetupWizard } from "./src/setup-surface.js";
6
+ export {
7
+ collectZalouserSecurityAuditFindings,
8
+ isZalouserMutableGroupEntry,
9
+ } from "./src/security-audit.js";
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "zalouser",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "channels": ["zalouser"],
7
+ "contracts": {
8
+ "tools": ["zalouser"]
9
+ },
10
+ "channelEnvVars": {
11
+ "zalouser": ["ZALOUSER_PROFILE", "ZCA_PROFILE"]
12
+ },
13
+ "configSchema": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {}
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ // Keep bundled channel entry imports narrow so bootstrap/discovery paths do
2
+ // not drag setup-only or tool runtime surfaces into lightweight plugin loads.
3
+ export { zalouserPlugin } from "./src/channel.js";
@@ -0,0 +1,2 @@
1
+ export { collectZalouserSecurityAuditFindings } from "./src/security-audit.js";
2
+ export { legacyConfigRules, normalizeCompatibilityConfig } from "./src/doctor-contract.js";
@@ -0,0 +1 @@
1
+ export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
package/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import {
2
+ type AnyAgentTool,
3
+ defineBundledChannelEntry,
4
+ loadBundledEntryExportSync,
5
+ } from "autobot/plugin-sdk/channel-entry-contract";
6
+
7
+ function createZalouserTool(context?: unknown): AnyAgentTool {
8
+ const createTool = loadBundledEntryExportSync<(context?: unknown) => AnyAgentTool>(
9
+ import.meta.url,
10
+ {
11
+ specifier: "./api.js",
12
+ exportName: "createZalouserTool",
13
+ },
14
+ );
15
+ return createTool(context);
16
+ }
17
+
18
+ export default defineBundledChannelEntry({
19
+ id: "zalouser",
20
+ name: "Zalo Personal",
21
+ description: "Zalo personal account messaging via native zca-js integration",
22
+ importMetaUrl: import.meta.url,
23
+ plugin: {
24
+ specifier: "./channel-plugin-api.js",
25
+ exportName: "zalouserPlugin",
26
+ },
27
+ runtime: {
28
+ specifier: "./runtime-api.js",
29
+ exportName: "setZalouserRuntime",
30
+ },
31
+ registerFull(api) {
32
+ api.registerTool((ctx) => createZalouserTool(ctx), { name: "zalouser" });
33
+ },
34
+ });
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@gakr-gakr/zalouser",
3
+ "version": "0.1.0",
4
+ "description": "AutoBot Zalo Personal Account plugin via native zca-js integration",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/autobot/autobot"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "typebox": "1.1.38",
12
+ "zca-js": "2.1.2",
13
+ "zod": "4.4.3"
14
+ },
15
+ "devDependencies": {
16
+ "@gakr-gakr/plugin-sdk": "workspace:*",
17
+ "@gakr-gakr/autobot": "workspace:*",
18
+ "autobot": "workspace:@gakr-gakr/autobot@*"
19
+ },
20
+ "peerDependencies": {
21
+ "@gakr-gakr/autobot": ">=0.1.0"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "@gakr-gakr/autobot": {
25
+ "optional": true
26
+ }
27
+ },
28
+ "autobot": {
29
+ "extensions": [
30
+ "./index.ts"
31
+ ],
32
+ "setupEntry": "./setup-entry.ts",
33
+ "channel": {
34
+ "id": "zalouser",
35
+ "label": "Zalo Personal",
36
+ "selectionLabel": "Zalo (Personal Account)",
37
+ "docsPath": "/channels/zalouser",
38
+ "docsLabel": "zalouser",
39
+ "blurb": "Zalo personal account via QR code login.",
40
+ "aliases": [
41
+ "zlu"
42
+ ],
43
+ "order": 85,
44
+ "quickstartAllowFrom": false,
45
+ "doctorCapabilities": {
46
+ "dmAllowFromMode": "topOnly",
47
+ "groupModel": "hybrid",
48
+ "groupAllowFromFallbackToAllowFrom": false,
49
+ "warnOnEmptyGroupSenderAllowlist": false
50
+ }
51
+ },
52
+ "install": {
53
+ "npmSpec": "@gakr-gakr/zalouser",
54
+ "defaultChoice": "npm",
55
+ "minHostVersion": ">=2026.4.10"
56
+ },
57
+ "compat": {
58
+ "pluginApi": ">=2026.5.19"
59
+ },
60
+ "build": {
61
+ "autobotVersion": "2026.5.19"
62
+ },
63
+ "release": {
64
+ "publishToClawHub": true,
65
+ "publishToNpm": true
66
+ }
67
+ }
68
+ }
package/runtime-api.ts ADDED
@@ -0,0 +1,62 @@
1
+ export {
2
+ collectZalouserSecurityAuditFindings,
3
+ createZalouserSetupWizardProxy,
4
+ createZalouserTool,
5
+ isZalouserMutableGroupEntry,
6
+ zalouserPlugin,
7
+ zalouserSetupAdapter,
8
+ zalouserSetupPlugin,
9
+ zalouserSetupWizard,
10
+ } from "./api.js";
11
+ export { setZalouserRuntime } from "./src/runtime.js";
12
+ export type { ReplyPayload } from "autobot/plugin-sdk/reply-runtime";
13
+ export type {
14
+ BaseProbeResult,
15
+ ChannelAccountSnapshot,
16
+ ChannelDirectoryEntry,
17
+ ChannelGroupContext,
18
+ ChannelMessageActionAdapter,
19
+ ChannelStatusIssue,
20
+ } from "autobot/plugin-sdk/channel-contract";
21
+ export type {
22
+ AutoBotConfig,
23
+ GroupToolPolicyConfig,
24
+ MarkdownTableMode,
25
+ } from "autobot/plugin-sdk/config-contracts";
26
+ export type {
27
+ PluginRuntime,
28
+ AnyAgentTool,
29
+ ChannelPlugin,
30
+ AutoBotPluginToolContext,
31
+ } from "autobot/plugin-sdk/core";
32
+ export type { RuntimeEnv } from "autobot/plugin-sdk/runtime";
33
+ export {
34
+ DEFAULT_ACCOUNT_ID,
35
+ buildChannelConfigSchema,
36
+ normalizeAccountId,
37
+ } from "autobot/plugin-sdk/core";
38
+ export { chunkTextForOutbound } from "autobot/plugin-sdk/text-chunking";
39
+ export { isDangerousNameMatchingEnabled } from "autobot/plugin-sdk/dangerous-name-runtime";
40
+ export {
41
+ resolveDefaultGroupPolicy,
42
+ resolveOpenProviderRuntimeGroupPolicy,
43
+ warnMissingProviderGroupPolicyFallbackOnce,
44
+ } from "autobot/plugin-sdk/runtime-group-policy";
45
+ export {
46
+ mergeAllowlist,
47
+ summarizeMapping,
48
+ formatAllowFromLowercase,
49
+ } from "autobot/plugin-sdk/allow-from";
50
+ export { resolveInboundMentionDecision } from "autobot/plugin-sdk/channel-inbound";
51
+ export { createChannelPairingController } from "autobot/plugin-sdk/channel-pairing";
52
+ export { createChannelMessageReplyPipeline } from "autobot/plugin-sdk/channel-message";
53
+ export { buildBaseAccountStatusSnapshot } from "autobot/plugin-sdk/status-helpers";
54
+ export { loadOutboundMediaFromUrl } from "autobot/plugin-sdk/outbound-media";
55
+ export {
56
+ deliverTextOrMediaReply,
57
+ isNumericTargetId,
58
+ resolveSendableOutboundReplyParts,
59
+ sendPayloadWithChunkedTextAndMedia,
60
+ type OutboundReplyPayload,
61
+ } from "autobot/plugin-sdk/reply-payload";
62
+ export { resolvePreferredAutoBotTmpDir } from "autobot/plugin-sdk/temp-path";
@@ -0,0 +1,4 @@
1
+ // Zalo User does not expose secret-contract surfaces.
2
+ export const secretTargetRegistryEntries: readonly [] = [];
3
+
4
+ export function collectRuntimeConfigAssignments(): void {}
package/setup-entry.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineBundledChannelSetupEntry } from "autobot/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "zalouserSetupPlugin",
8
+ },
9
+ });
@@ -0,0 +1,2 @@
1
+ // Keep setup-entry imports narrow so setup loads do not pull tool surfaces.
2
+ export { zalouserSetupPlugin } from "./src/channel.setup.js";
@@ -0,0 +1 @@
1
+ export { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
@@ -0,0 +1,14 @@
1
+ import { vi } from "vitest";
2
+ import { createDefaultResolvedZalouserAccount } from "./test-helpers.js";
3
+
4
+ vi.mock("./accounts.js", () => {
5
+ return {
6
+ listZalouserAccountIds: () => ["default"],
7
+ resolveDefaultZalouserAccountId: () => "default",
8
+ resolveZalouserAccountSync: () => createDefaultResolvedZalouserAccount(),
9
+ resolveZalouserAccount: async () => createDefaultResolvedZalouserAccount(),
10
+ listEnabledZalouserAccounts: async () => [createDefaultResolvedZalouserAccount()],
11
+ getZcaUserInfo: async () => null,
12
+ checkZcaAuthenticated: async () => false,
13
+ };
14
+ });
@@ -0,0 +1,136 @@
1
+ import {
2
+ createAccountListHelpers,
3
+ DEFAULT_ACCOUNT_ID,
4
+ normalizeAccountId,
5
+ resolveMergedAccountConfig,
6
+ } from "autobot/plugin-sdk/account-resolution";
7
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
8
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
9
+ import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
10
+
11
+ let zalouserAccountsRuntimePromise: Promise<typeof import("./accounts.runtime.js")> | undefined;
12
+
13
+ async function loadZalouserAccountsRuntime() {
14
+ zalouserAccountsRuntimePromise ??= import("./accounts.runtime.js");
15
+ return await zalouserAccountsRuntimePromise;
16
+ }
17
+
18
+ const {
19
+ listAccountIds: listZalouserAccountIds,
20
+ resolveDefaultAccountId: resolveDefaultZalouserAccountId,
21
+ } = createAccountListHelpers("zalouser", {
22
+ implicitDefaultAccount: {
23
+ channelKeys: ["profile"],
24
+ envVars: ["ZALOUSER_PROFILE", "ZCA_PROFILE"],
25
+ },
26
+ });
27
+ export { listZalouserAccountIds, resolveDefaultZalouserAccountId };
28
+
29
+ function mergeZalouserAccountConfig(cfg: AutoBotConfig, accountId: string): ZalouserAccountConfig {
30
+ const merged = resolveMergedAccountConfig<ZalouserAccountConfig>({
31
+ channelConfig: cfg.channels?.zalouser as ZalouserAccountConfig | undefined,
32
+ accounts: (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts as
33
+ | Record<string, Partial<ZalouserAccountConfig>>
34
+ | undefined,
35
+ accountId,
36
+ omitKeys: ["defaultAccount"],
37
+ });
38
+ return {
39
+ ...merged,
40
+ // Match Telegram's safe default: groups stay allowlisted unless explicitly opened.
41
+ groupPolicy: merged.groupPolicy ?? "allowlist",
42
+ };
43
+ }
44
+
45
+ function resolveProfile(config: ZalouserAccountConfig, accountId: string): string {
46
+ if (config.profile?.trim()) {
47
+ return config.profile.trim();
48
+ }
49
+ if (process.env.ZALOUSER_PROFILE?.trim()) {
50
+ return process.env.ZALOUSER_PROFILE.trim();
51
+ }
52
+ if (process.env.ZCA_PROFILE?.trim()) {
53
+ return process.env.ZCA_PROFILE.trim();
54
+ }
55
+ if (accountId !== DEFAULT_ACCOUNT_ID) {
56
+ return accountId;
57
+ }
58
+ return "default";
59
+ }
60
+
61
+ function resolveZalouserAccountBase(params: { cfg: AutoBotConfig; accountId?: string | null }) {
62
+ const accountId = normalizeAccountId(
63
+ params.accountId ?? resolveDefaultZalouserAccountId(params.cfg),
64
+ );
65
+ const baseEnabled =
66
+ (params.cfg.channels?.zalouser as ZalouserConfig | undefined)?.enabled !== false;
67
+ const merged = mergeZalouserAccountConfig(params.cfg, accountId);
68
+ return {
69
+ accountId,
70
+ enabled: baseEnabled && merged.enabled !== false,
71
+ merged,
72
+ profile: resolveProfile(merged, accountId),
73
+ };
74
+ }
75
+
76
+ export async function resolveZalouserAccount(params: {
77
+ cfg: AutoBotConfig;
78
+ accountId?: string | null;
79
+ }): Promise<ResolvedZalouserAccount> {
80
+ const { accountId, enabled, merged, profile } = resolveZalouserAccountBase(params);
81
+ const authenticated = await (await loadZalouserAccountsRuntime()).checkZaloAuthenticated(profile);
82
+
83
+ return {
84
+ accountId,
85
+ name: normalizeOptionalString(merged.name),
86
+ enabled,
87
+ profile,
88
+ authenticated,
89
+ config: merged,
90
+ };
91
+ }
92
+
93
+ export function resolveZalouserAccountSync(params: {
94
+ cfg: AutoBotConfig;
95
+ accountId?: string | null;
96
+ }): ResolvedZalouserAccount {
97
+ const { accountId, enabled, merged, profile } = resolveZalouserAccountBase(params);
98
+
99
+ return {
100
+ accountId,
101
+ name: normalizeOptionalString(merged.name),
102
+ enabled,
103
+ profile,
104
+ authenticated: false,
105
+ config: merged,
106
+ };
107
+ }
108
+
109
+ export async function listEnabledZalouserAccounts(
110
+ cfg: AutoBotConfig,
111
+ ): Promise<ResolvedZalouserAccount[]> {
112
+ const ids = listZalouserAccountIds(cfg);
113
+ const accounts = await Promise.all(
114
+ ids.map((accountId) => resolveZalouserAccount({ cfg, accountId })),
115
+ );
116
+ return accounts.filter((account) => account.enabled);
117
+ }
118
+
119
+ export async function getZcaUserInfo(
120
+ profile: string,
121
+ ): Promise<{ userId?: string; displayName?: string } | null> {
122
+ const info = await (await loadZalouserAccountsRuntime()).getZaloUserInfo(profile);
123
+ if (!info) {
124
+ return null;
125
+ }
126
+ return {
127
+ userId: info.userId,
128
+ displayName: info.displayName,
129
+ };
130
+ }
131
+
132
+ export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
133
+ return await (await loadZalouserAccountsRuntime()).checkZaloAuthenticated(profile);
134
+ }
135
+
136
+ export type { ResolvedZalouserAccount } from "./types.js";
@@ -0,0 +1,20 @@
1
+ export { formatAllowFromLowercase } from "autobot/plugin-sdk/allow-from";
2
+ export type {
3
+ ChannelDirectoryEntry,
4
+ ChannelGroupContext,
5
+ ChannelMessageActionAdapter,
6
+ } from "autobot/plugin-sdk/channel-contract";
7
+ export { buildChannelConfigSchema } from "autobot/plugin-sdk/channel-config-schema";
8
+ export type { ChannelPlugin } from "autobot/plugin-sdk/core";
9
+ export {
10
+ DEFAULT_ACCOUNT_ID,
11
+ normalizeAccountId,
12
+ type AutoBotConfig,
13
+ } from "autobot/plugin-sdk/core";
14
+ export { isDangerousNameMatchingEnabled } from "autobot/plugin-sdk/dangerous-name-runtime";
15
+ export type { GroupToolPolicyConfig } from "autobot/plugin-sdk/config-contracts";
16
+ export { chunkTextForOutbound } from "autobot/plugin-sdk/text-chunking";
17
+ export {
18
+ isNumericTargetId,
19
+ sendPayloadWithChunkedTextAndMedia,
20
+ } from "autobot/plugin-sdk/reply-payload";