@openclaw/zalo 2026.2.25 → 2026.3.2
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/CHANGELOG.md +18 -0
- package/index.ts +0 -2
- package/package.json +1 -1
- package/src/accounts.ts +13 -3
- package/src/channel.sendpayload.test.ts +102 -0
- package/src/channel.ts +36 -1
- package/src/config-schema.test.ts +30 -0
- package/src/config-schema.ts +3 -2
- package/src/monitor.ts +98 -78
- package/src/monitor.webhook.test.ts +183 -5
- package/src/monitor.webhook.ts +80 -71
- package/src/onboarding.status.test.ts +24 -0
- package/src/onboarding.ts +89 -64
- package/src/secret-input.ts +19 -0
- package/src/token.test.ts +58 -0
- package/src/token.ts +50 -9
- package/src/types.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.2
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.1
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.2.26
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.2.25
|
|
4
22
|
|
|
5
23
|
### Changes
|
package/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
3
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
|
4
|
-
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
|
5
4
|
import { setZaloRuntime } from "./src/runtime.js";
|
|
6
5
|
|
|
7
6
|
const plugin = {
|
|
@@ -12,7 +11,6 @@ const plugin = {
|
|
|
12
11
|
register(api: OpenClawPluginApi) {
|
|
13
12
|
setZaloRuntime(api.runtime);
|
|
14
13
|
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
|
15
|
-
api.registerHttpHandler(handleZaloWebhookRequest);
|
|
16
14
|
},
|
|
17
15
|
};
|
|
18
16
|
|
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
normalizeAccountId,
|
|
5
|
+
normalizeOptionalAccountId,
|
|
6
|
+
} from "openclaw/plugin-sdk/account-id";
|
|
3
7
|
import { resolveZaloToken } from "./token.js";
|
|
4
8
|
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
|
5
9
|
|
|
@@ -23,8 +27,12 @@ export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
|
|
|
23
27
|
|
|
24
28
|
export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
|
|
25
29
|
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const preferred = normalizeOptionalAccountId(zaloConfig?.defaultAccount);
|
|
31
|
+
if (
|
|
32
|
+
preferred &&
|
|
33
|
+
listZaloAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
34
|
+
) {
|
|
35
|
+
return preferred;
|
|
28
36
|
}
|
|
29
37
|
const ids = listZaloAccountIds(cfg);
|
|
30
38
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
@@ -54,6 +62,7 @@ function mergeZaloAccountConfig(cfg: OpenClawConfig, accountId: string): ZaloAcc
|
|
|
54
62
|
export function resolveZaloAccount(params: {
|
|
55
63
|
cfg: OpenClawConfig;
|
|
56
64
|
accountId?: string | null;
|
|
65
|
+
allowUnresolvedSecretRef?: boolean;
|
|
57
66
|
}): ResolvedZaloAccount {
|
|
58
67
|
const accountId = normalizeAccountId(params.accountId);
|
|
59
68
|
const baseEnabled = (params.cfg.channels?.zalo as ZaloConfig | undefined)?.enabled !== false;
|
|
@@ -63,6 +72,7 @@ export function resolveZaloAccount(params: {
|
|
|
63
72
|
const tokenResolution = resolveZaloToken(
|
|
64
73
|
params.cfg.channels?.zalo as ZaloConfig | undefined,
|
|
65
74
|
accountId,
|
|
75
|
+
{ allowUnresolvedSecretRef: params.allowUnresolvedSecretRef },
|
|
66
76
|
);
|
|
67
77
|
|
|
68
78
|
return {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ReplyPayload } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { zaloPlugin } from "./channel.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./send.js", () => ({
|
|
6
|
+
sendMessageZalo: vi.fn().mockResolvedValue({ ok: true, messageId: "zl-1" }),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
function baseCtx(payload: ReplyPayload) {
|
|
10
|
+
return {
|
|
11
|
+
cfg: {},
|
|
12
|
+
to: "123456789",
|
|
13
|
+
text: "",
|
|
14
|
+
payload,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("zaloPlugin outbound sendPayload", () => {
|
|
19
|
+
let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalo"]>>;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
const mod = await import("./send.js");
|
|
23
|
+
mockedSend = vi.mocked(mod.sendMessageZalo);
|
|
24
|
+
mockedSend.mockClear();
|
|
25
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-1" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("text-only delegates to sendText", async () => {
|
|
29
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-t1" });
|
|
30
|
+
|
|
31
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: "hello" }));
|
|
32
|
+
|
|
33
|
+
expect(mockedSend).toHaveBeenCalledWith("123456789", "hello", expect.any(Object));
|
|
34
|
+
expect(result).toMatchObject({ channel: "zalo", messageId: "zl-t1" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("single media delegates to sendMedia", async () => {
|
|
38
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-m1" });
|
|
39
|
+
|
|
40
|
+
const result = await zaloPlugin.outbound!.sendPayload!(
|
|
41
|
+
baseCtx({ text: "cap", mediaUrl: "https://example.com/a.jpg" }),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
45
|
+
"123456789",
|
|
46
|
+
"cap",
|
|
47
|
+
expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }),
|
|
48
|
+
);
|
|
49
|
+
expect(result).toMatchObject({ channel: "zalo" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("multi-media iterates URLs with caption on first", async () => {
|
|
53
|
+
mockedSend
|
|
54
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-1" })
|
|
55
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-2" });
|
|
56
|
+
|
|
57
|
+
const result = await zaloPlugin.outbound!.sendPayload!(
|
|
58
|
+
baseCtx({
|
|
59
|
+
text: "caption",
|
|
60
|
+
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(mockedSend).toHaveBeenCalledTimes(2);
|
|
65
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
66
|
+
1,
|
|
67
|
+
"123456789",
|
|
68
|
+
"caption",
|
|
69
|
+
expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }),
|
|
70
|
+
);
|
|
71
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
72
|
+
2,
|
|
73
|
+
"123456789",
|
|
74
|
+
"",
|
|
75
|
+
expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }),
|
|
76
|
+
);
|
|
77
|
+
expect(result).toMatchObject({ channel: "zalo", messageId: "zl-2" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("empty payload returns no-op", async () => {
|
|
81
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({}));
|
|
82
|
+
|
|
83
|
+
expect(mockedSend).not.toHaveBeenCalled();
|
|
84
|
+
expect(result).toEqual({ channel: "zalo", messageId: "" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("chunking splits long text", async () => {
|
|
88
|
+
mockedSend
|
|
89
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-c1" })
|
|
90
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-c2" });
|
|
91
|
+
|
|
92
|
+
const longText = "a".repeat(3000);
|
|
93
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: longText }));
|
|
94
|
+
|
|
95
|
+
// textChunkLimit is 2000 with chunkTextForOutbound, so it should split
|
|
96
|
+
expect(mockedSend.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
97
|
+
for (const call of mockedSend.mock.calls) {
|
|
98
|
+
expect((call[1] as string).length).toBeLessThanOrEqual(2000);
|
|
99
|
+
}
|
|
100
|
+
expect(result).toMatchObject({ channel: "zalo" });
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/channel.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { ZaloConfigSchema } from "./config-schema.js";
|
|
|
32
32
|
import { zaloOnboardingAdapter } from "./onboarding.js";
|
|
33
33
|
import { probeZalo } from "./probe.js";
|
|
34
34
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
35
|
+
import { normalizeSecretInputString } from "./secret-input.js";
|
|
35
36
|
import { sendMessageZalo } from "./send.js";
|
|
36
37
|
import { collectZaloStatusIssues } from "./status-issues.js";
|
|
37
38
|
|
|
@@ -302,6 +303,40 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
302
303
|
chunker: chunkTextForOutbound,
|
|
303
304
|
chunkerMode: "text",
|
|
304
305
|
textChunkLimit: 2000,
|
|
306
|
+
sendPayload: async (ctx) => {
|
|
307
|
+
const text = ctx.payload.text ?? "";
|
|
308
|
+
const urls = ctx.payload.mediaUrls?.length
|
|
309
|
+
? ctx.payload.mediaUrls
|
|
310
|
+
: ctx.payload.mediaUrl
|
|
311
|
+
? [ctx.payload.mediaUrl]
|
|
312
|
+
: [];
|
|
313
|
+
if (!text && urls.length === 0) {
|
|
314
|
+
return { channel: "zalo", messageId: "" };
|
|
315
|
+
}
|
|
316
|
+
if (urls.length > 0) {
|
|
317
|
+
let lastResult = await zaloPlugin.outbound!.sendMedia!({
|
|
318
|
+
...ctx,
|
|
319
|
+
text,
|
|
320
|
+
mediaUrl: urls[0],
|
|
321
|
+
});
|
|
322
|
+
for (let i = 1; i < urls.length; i++) {
|
|
323
|
+
lastResult = await zaloPlugin.outbound!.sendMedia!({
|
|
324
|
+
...ctx,
|
|
325
|
+
text: "",
|
|
326
|
+
mediaUrl: urls[i],
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return lastResult;
|
|
330
|
+
}
|
|
331
|
+
const outbound = zaloPlugin.outbound!;
|
|
332
|
+
const limit = outbound.textChunkLimit;
|
|
333
|
+
const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];
|
|
334
|
+
let lastResult: Awaited<ReturnType<NonNullable<typeof outbound.sendText>>>;
|
|
335
|
+
for (const chunk of chunks) {
|
|
336
|
+
lastResult = await outbound.sendText!({ ...ctx, text: chunk });
|
|
337
|
+
}
|
|
338
|
+
return lastResult!;
|
|
339
|
+
},
|
|
305
340
|
sendText: async ({ to, text, accountId, cfg }) => {
|
|
306
341
|
const result = await sendMessageZalo(to, text, {
|
|
307
342
|
accountId: accountId ?? undefined,
|
|
@@ -388,7 +423,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
388
423
|
abortSignal: ctx.abortSignal,
|
|
389
424
|
useWebhook: Boolean(account.config.webhookUrl),
|
|
390
425
|
webhookUrl: account.config.webhookUrl,
|
|
391
|
-
webhookSecret: account.config.webhookSecret,
|
|
426
|
+
webhookSecret: normalizeSecretInputString(account.config.webhookSecret),
|
|
392
427
|
webhookPath: account.config.webhookPath,
|
|
393
428
|
fetcher,
|
|
394
429
|
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ZaloConfigSchema } from "./config-schema.js";
|
|
3
|
+
|
|
4
|
+
describe("ZaloConfigSchema SecretInput", () => {
|
|
5
|
+
it("accepts SecretRef botToken and webhookSecret at top-level", () => {
|
|
6
|
+
const result = ZaloConfigSchema.safeParse({
|
|
7
|
+
botToken: { source: "env", provider: "default", id: "ZALO_BOT_TOKEN" },
|
|
8
|
+
webhookUrl: "https://example.com/zalo",
|
|
9
|
+
webhookSecret: { source: "env", provider: "default", id: "ZALO_WEBHOOK_SECRET" },
|
|
10
|
+
});
|
|
11
|
+
expect(result.success).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts SecretRef botToken and webhookSecret on account", () => {
|
|
15
|
+
const result = ZaloConfigSchema.safeParse({
|
|
16
|
+
accounts: {
|
|
17
|
+
work: {
|
|
18
|
+
botToken: { source: "env", provider: "default", id: "ZALO_WORK_BOT_TOKEN" },
|
|
19
|
+
webhookUrl: "https://example.com/zalo/work",
|
|
20
|
+
webhookSecret: {
|
|
21
|
+
source: "env",
|
|
22
|
+
provider: "default",
|
|
23
|
+
id: "ZALO_WORK_WEBHOOK_SECRET",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(result.success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MarkdownConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { buildSecretInputSchema } from "./secret-input.js";
|
|
3
4
|
|
|
4
5
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
|
5
6
|
|
|
@@ -7,10 +8,10 @@ const zaloAccountSchema = z.object({
|
|
|
7
8
|
name: z.string().optional(),
|
|
8
9
|
enabled: z.boolean().optional(),
|
|
9
10
|
markdown: MarkdownConfigSchema,
|
|
10
|
-
botToken:
|
|
11
|
+
botToken: buildSecretInputSchema().optional(),
|
|
11
12
|
tokenFile: z.string().optional(),
|
|
12
13
|
webhookUrl: z.string().optional(),
|
|
13
|
-
webhookSecret:
|
|
14
|
+
webhookSecret: buildSecretInputSchema().optional(),
|
|
14
15
|
webhookPath: z.string().optional(),
|
|
15
16
|
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
|
16
17
|
allowFrom: z.array(allowFromEntry).optional(),
|
package/src/monitor.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
|
|
3
3
|
import {
|
|
4
|
+
createScopedPairingAccess,
|
|
4
5
|
createReplyPrefixOptions,
|
|
5
|
-
|
|
6
|
+
resolveDirectDmAuthorizationOutcome,
|
|
7
|
+
resolveSenderCommandAuthorizationWithRuntime,
|
|
6
8
|
resolveOutboundMediaUrls,
|
|
7
9
|
resolveDefaultGroupPolicy,
|
|
10
|
+
resolveInboundRouteEnvelopeBuilderWithRuntime,
|
|
8
11
|
sendMediaWithLeadingCaption,
|
|
9
12
|
resolveWebhookPath,
|
|
10
13
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
@@ -27,6 +30,9 @@ import {
|
|
|
27
30
|
resolveZaloRuntimeGroupPolicy,
|
|
28
31
|
} from "./group-access.js";
|
|
29
32
|
import {
|
|
33
|
+
clearZaloWebhookSecurityStateForTest,
|
|
34
|
+
getZaloWebhookRateLimitStateSizeForTest,
|
|
35
|
+
getZaloWebhookStatusCounterSizeForTest,
|
|
30
36
|
handleZaloWebhookRequest as handleZaloWebhookRequestInternal,
|
|
31
37
|
registerZaloWebhookTarget as registerZaloWebhookTargetInternal,
|
|
32
38
|
type ZaloWebhookTarget,
|
|
@@ -69,9 +75,32 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export function registerZaloWebhookTarget(target: ZaloWebhookTarget): () => void {
|
|
72
|
-
return registerZaloWebhookTargetInternal(target
|
|
78
|
+
return registerZaloWebhookTargetInternal(target, {
|
|
79
|
+
route: {
|
|
80
|
+
auth: "plugin",
|
|
81
|
+
match: "exact",
|
|
82
|
+
pluginId: "zalo",
|
|
83
|
+
source: "zalo-webhook",
|
|
84
|
+
accountId: target.account.accountId,
|
|
85
|
+
log: target.runtime.log,
|
|
86
|
+
handler: async (req, res) => {
|
|
87
|
+
const handled = await handleZaloWebhookRequest(req, res);
|
|
88
|
+
if (!handled && !res.headersSent) {
|
|
89
|
+
res.statusCode = 404;
|
|
90
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
91
|
+
res.end("Not Found");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
73
96
|
}
|
|
74
97
|
|
|
98
|
+
export {
|
|
99
|
+
clearZaloWebhookSecurityStateForTest,
|
|
100
|
+
getZaloWebhookRateLimitStateSizeForTest,
|
|
101
|
+
getZaloWebhookStatusCounterSizeForTest,
|
|
102
|
+
};
|
|
103
|
+
|
|
75
104
|
export async function handleZaloWebhookRequest(
|
|
76
105
|
req: IncomingMessage,
|
|
77
106
|
res: ServerResponse,
|
|
@@ -142,7 +171,7 @@ function startPollingLoop(params: {
|
|
|
142
171
|
if (err instanceof ZaloApiError && err.isPollingTimeout) {
|
|
143
172
|
// no updates
|
|
144
173
|
} else if (!isStopped() && !abortSignal.aborted) {
|
|
145
|
-
|
|
174
|
+
runtime.error?.(`[${account.accountId}] Zalo polling error: ${String(err)}`);
|
|
146
175
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
147
176
|
}
|
|
148
177
|
}
|
|
@@ -189,10 +218,12 @@ async function processUpdate(
|
|
|
189
218
|
);
|
|
190
219
|
break;
|
|
191
220
|
case "message.sticker.received":
|
|
192
|
-
|
|
221
|
+
logVerbose(core, runtime, `[${account.accountId}] Received sticker from ${message.from.id}`);
|
|
193
222
|
break;
|
|
194
223
|
case "message.unsupported.received":
|
|
195
|
-
|
|
224
|
+
logVerbose(
|
|
225
|
+
core,
|
|
226
|
+
runtime,
|
|
196
227
|
`[${account.accountId}] Received unsupported message type from ${message.from.id}`,
|
|
197
228
|
);
|
|
198
229
|
break;
|
|
@@ -258,7 +289,7 @@ async function handleImageMessage(
|
|
|
258
289
|
mediaPath = saved.path;
|
|
259
290
|
mediaType = saved.contentType;
|
|
260
291
|
} catch (err) {
|
|
261
|
-
|
|
292
|
+
runtime.error?.(`[${account.accountId}] Failed to download Zalo image: ${String(err)}`);
|
|
262
293
|
}
|
|
263
294
|
}
|
|
264
295
|
|
|
@@ -303,6 +334,11 @@ async function processMessageWithPipeline(params: {
|
|
|
303
334
|
statusSink,
|
|
304
335
|
fetcher,
|
|
305
336
|
} = params;
|
|
337
|
+
const pairing = createScopedPairingAccess({
|
|
338
|
+
core,
|
|
339
|
+
channel: "zalo",
|
|
340
|
+
accountId: account.accountId,
|
|
341
|
+
});
|
|
306
342
|
const { from, chat, message_id, date } = message;
|
|
307
343
|
|
|
308
344
|
const isGroup = chat.chat_type === "GROUP";
|
|
@@ -349,82 +385,76 @@ async function processMessageWithPipeline(params: {
|
|
|
349
385
|
}
|
|
350
386
|
|
|
351
387
|
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
|
|
352
|
-
const { senderAllowedForCommands, commandAuthorized } =
|
|
353
|
-
|
|
354
|
-
|
|
388
|
+
const { senderAllowedForCommands, commandAuthorized } =
|
|
389
|
+
await resolveSenderCommandAuthorizationWithRuntime({
|
|
390
|
+
cfg: config,
|
|
391
|
+
rawBody,
|
|
392
|
+
isGroup,
|
|
393
|
+
dmPolicy,
|
|
394
|
+
configuredAllowFrom: configAllowFrom,
|
|
395
|
+
configuredGroupAllowFrom: groupAllowFrom,
|
|
396
|
+
senderId,
|
|
397
|
+
isSenderAllowed: isZaloSenderAllowed,
|
|
398
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
399
|
+
runtime: core.channel.commands,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const directDmOutcome = resolveDirectDmAuthorizationOutcome({
|
|
355
403
|
isGroup,
|
|
356
404
|
dmPolicy,
|
|
357
|
-
|
|
358
|
-
senderId,
|
|
359
|
-
isSenderAllowed: isZaloSenderAllowed,
|
|
360
|
-
readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
|
|
361
|
-
shouldComputeCommandAuthorized: (body, cfg) =>
|
|
362
|
-
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
|
363
|
-
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
|
364
|
-
core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
|
|
405
|
+
senderAllowedForCommands,
|
|
365
406
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
text: core.channel.pairing.buildPairingReply({
|
|
392
|
-
channel: "zalo",
|
|
393
|
-
idLine: `Your Zalo user id: ${senderId}`,
|
|
394
|
-
code,
|
|
395
|
-
}),
|
|
396
|
-
},
|
|
397
|
-
fetcher,
|
|
398
|
-
);
|
|
399
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
400
|
-
} catch (err) {
|
|
401
|
-
logVerbose(
|
|
402
|
-
core,
|
|
403
|
-
runtime,
|
|
404
|
-
`zalo pairing reply failed for ${senderId}: ${String(err)}`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
logVerbose(
|
|
410
|
-
core,
|
|
411
|
-
runtime,
|
|
412
|
-
`Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
407
|
+
if (directDmOutcome === "disabled") {
|
|
408
|
+
logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (directDmOutcome === "unauthorized") {
|
|
412
|
+
if (dmPolicy === "pairing") {
|
|
413
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
414
|
+
id: senderId,
|
|
415
|
+
meta: { name: senderName ?? undefined },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (created) {
|
|
419
|
+
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
|
420
|
+
try {
|
|
421
|
+
await sendMessage(
|
|
422
|
+
token,
|
|
423
|
+
{
|
|
424
|
+
chat_id: chatId,
|
|
425
|
+
text: core.channel.pairing.buildPairingReply({
|
|
426
|
+
channel: "zalo",
|
|
427
|
+
idLine: `Your Zalo user id: ${senderId}`,
|
|
428
|
+
code,
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
fetcher,
|
|
413
432
|
);
|
|
433
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
434
|
+
} catch (err) {
|
|
435
|
+
logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
|
|
414
436
|
}
|
|
415
|
-
return;
|
|
416
437
|
}
|
|
438
|
+
} else {
|
|
439
|
+
logVerbose(
|
|
440
|
+
core,
|
|
441
|
+
runtime,
|
|
442
|
+
`Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
443
|
+
);
|
|
417
444
|
}
|
|
445
|
+
return;
|
|
418
446
|
}
|
|
419
447
|
|
|
420
|
-
const route =
|
|
448
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
421
449
|
cfg: config,
|
|
422
450
|
channel: "zalo",
|
|
423
451
|
accountId: account.accountId,
|
|
424
452
|
peer: {
|
|
425
|
-
kind: isGroup ? "group" : "direct",
|
|
453
|
+
kind: isGroup ? ("group" as const) : ("direct" as const),
|
|
426
454
|
id: chatId,
|
|
427
455
|
},
|
|
456
|
+
runtime: core.channel,
|
|
457
|
+
sessionStore: config.session?.store,
|
|
428
458
|
});
|
|
429
459
|
|
|
430
460
|
if (
|
|
@@ -437,20 +467,10 @@ async function processMessageWithPipeline(params: {
|
|
|
437
467
|
}
|
|
438
468
|
|
|
439
469
|
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
|
440
|
-
const storePath =
|
|
441
|
-
agentId: route.agentId,
|
|
442
|
-
});
|
|
443
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
|
444
|
-
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
445
|
-
storePath,
|
|
446
|
-
sessionKey: route.sessionKey,
|
|
447
|
-
});
|
|
448
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
470
|
+
const { storePath, body } = buildEnvelope({
|
|
449
471
|
channel: "Zalo",
|
|
450
472
|
from: fromLabel,
|
|
451
473
|
timestamp: date ? date * 1000 : undefined,
|
|
452
|
-
previousTimestamp,
|
|
453
|
-
envelope: envelopeOptions,
|
|
454
474
|
body: rawBody,
|
|
455
475
|
});
|
|
456
476
|
|