@openclaw/zalo 2026.3.12 → 2026.5.1-beta.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/README.md +1 -1
- package/api.ts +9 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +15 -0
- package/index.ts +16 -13
- package/openclaw.plugin.json +514 -1
- package/package.json +31 -5
- package/runtime-api.test.ts +17 -0
- package/runtime-api.ts +75 -0
- package/secret-contract-api.ts +5 -0
- package/setup-api.ts +34 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +70 -0
- package/src/accounts.ts +19 -19
- package/src/actions.runtime.ts +5 -0
- package/src/actions.test.ts +32 -0
- package/src/actions.ts +20 -14
- package/src/api.test.ts +108 -22
- package/src/api.ts +29 -2
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +25 -0
- package/src/channel.directory.test.ts +22 -16
- package/src/channel.runtime.ts +93 -0
- package/src/channel.startup.test.ts +36 -35
- package/src/channel.ts +228 -336
- package/src/config-schema.ts +3 -3
- package/src/group-access.ts +4 -3
- package/src/monitor.group-policy.test.ts +0 -12
- package/src/monitor.image.polling.test.ts +110 -0
- package/src/monitor.lifecycle.test.ts +77 -92
- package/src/monitor.pairing.lifecycle.test.ts +141 -0
- package/src/monitor.polling.media-reply.test.ts +425 -0
- package/src/monitor.reply-once.lifecycle.test.ts +171 -0
- package/src/monitor.ts +527 -304
- package/src/monitor.types.ts +4 -0
- package/src/monitor.webhook.test.ts +392 -62
- package/src/monitor.webhook.ts +73 -36
- package/src/outbound-media.test.ts +182 -0
- package/src/outbound-media.ts +241 -0
- package/src/outbound-payload.contract.test.ts +45 -0
- package/src/probe.ts +1 -1
- package/src/proxy.ts +1 -1
- package/src/runtime-api.ts +75 -0
- package/src/runtime-support.ts +91 -0
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +109 -0
- package/src/secret-input.ts +1 -9
- package/src/send.test.ts +120 -0
- package/src/send.ts +64 -40
- package/src/session-route.ts +32 -0
- package/src/setup-allow-from.ts +94 -0
- package/src/setup-core.ts +149 -0
- package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
- package/src/setup-surface.test.ts +175 -0
- package/src/{onboarding.ts → setup-surface.ts} +59 -177
- package/src/status-issues.test.ts +17 -0
- package/src/status-issues.ts +11 -27
- package/src/test-support/lifecycle-test-support.ts +413 -0
- package/src/test-support/monitor-mocks-test-support.ts +209 -0
- package/src/token.test.ts +15 -0
- package/src/token.ts +8 -17
- package/src/types.ts +2 -2
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -95
- package/src/channel.sendpayload.test.ts +0 -44
package/src/monitor.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
OutboundReplyPayload,
|
|
6
|
-
} from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
|
3
|
+
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
4
|
+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
7
5
|
import {
|
|
8
|
-
createTypingCallbacks,
|
|
9
|
-
createScopedPairingAccess,
|
|
10
|
-
createReplyPrefixOptions,
|
|
11
|
-
issuePairingChallenge,
|
|
12
|
-
logTypingFailure,
|
|
13
6
|
resolveDirectDmAuthorizationOutcome,
|
|
14
7
|
resolveSenderCommandAuthorizationWithRuntime,
|
|
15
|
-
|
|
8
|
+
} from "openclaw/plugin-sdk/command-auth";
|
|
9
|
+
import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
10
|
+
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
|
11
|
+
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
12
|
+
import {
|
|
13
|
+
deliverTextOrMediaReply,
|
|
14
|
+
type OutboundReplyPayload,
|
|
15
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
16
|
+
import { waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
17
|
+
import {
|
|
16
18
|
resolveDefaultGroupPolicy,
|
|
17
|
-
resolveInboundRouteEnvelopeBuilderWithRuntime,
|
|
18
|
-
sendMediaWithLeadingCaption,
|
|
19
|
-
resolveWebhookPath,
|
|
20
|
-
waitForAbortSignal,
|
|
21
19
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
22
|
-
} from "openclaw/plugin-sdk/
|
|
20
|
+
} from "openclaw/plugin-sdk/runtime-group-policy";
|
|
21
|
+
import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
23
22
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
24
23
|
import {
|
|
25
24
|
ZaloApiError,
|
|
@@ -39,21 +38,15 @@ import {
|
|
|
39
38
|
isZaloSenderAllowed,
|
|
40
39
|
resolveZaloRuntimeGroupPolicy,
|
|
41
40
|
} from "./group-access.js";
|
|
42
|
-
import {
|
|
43
|
-
clearZaloWebhookSecurityStateForTest,
|
|
44
|
-
getZaloWebhookRateLimitStateSizeForTest,
|
|
45
|
-
getZaloWebhookStatusCounterSizeForTest,
|
|
46
|
-
handleZaloWebhookRequest as handleZaloWebhookRequestInternal,
|
|
47
|
-
registerZaloWebhookTarget as registerZaloWebhookTargetInternal,
|
|
48
|
-
type ZaloWebhookTarget,
|
|
49
|
-
} from "./monitor.webhook.js";
|
|
50
41
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
51
42
|
import { getZaloRuntime } from "./runtime.js";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
export type { ZaloRuntimeEnv } from "./monitor.types.js";
|
|
44
|
+
import type { ZaloRuntimeEnv } from "./monitor.types.js";
|
|
45
|
+
import {
|
|
46
|
+
prepareHostedZaloMediaUrl,
|
|
47
|
+
resolveHostedZaloMediaRoutePrefix,
|
|
48
|
+
tryHandleHostedZaloMediaRequest,
|
|
49
|
+
} from "./outbound-media.js";
|
|
57
50
|
|
|
58
51
|
export type ZaloMonitorOptions = {
|
|
59
52
|
token: string;
|
|
@@ -75,6 +68,115 @@ const WEBHOOK_CLEANUP_TIMEOUT_MS = 5_000;
|
|
|
75
68
|
const ZALO_TYPING_TIMEOUT_MS = 5_000;
|
|
76
69
|
|
|
77
70
|
type ZaloCoreRuntime = ReturnType<typeof getZaloRuntime>;
|
|
71
|
+
type ZaloStatusSink = (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
72
|
+
type ZaloWebhookModule = typeof import("./monitor.webhook.js");
|
|
73
|
+
type ZaloProcessingContext = {
|
|
74
|
+
token: string;
|
|
75
|
+
account: ResolvedZaloAccount;
|
|
76
|
+
config: OpenClawConfig;
|
|
77
|
+
runtime: ZaloRuntimeEnv;
|
|
78
|
+
core: ZaloCoreRuntime;
|
|
79
|
+
mediaMaxMb: number;
|
|
80
|
+
canHostMedia: boolean;
|
|
81
|
+
webhookUrl?: string;
|
|
82
|
+
webhookPath?: string;
|
|
83
|
+
statusSink?: ZaloStatusSink;
|
|
84
|
+
fetcher?: ZaloFetch;
|
|
85
|
+
};
|
|
86
|
+
type ZaloPollingLoopParams = ZaloProcessingContext & {
|
|
87
|
+
abortSignal: AbortSignal;
|
|
88
|
+
isStopped: () => boolean;
|
|
89
|
+
};
|
|
90
|
+
type ZaloUpdateProcessingParams = ZaloProcessingContext & {
|
|
91
|
+
update: ZaloUpdate;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let zaloWebhookModulePromise: Promise<ZaloWebhookModule> | undefined;
|
|
95
|
+
const hostedMediaRouteRefs = new Map<string, { count: number; unregisters: Array<() => void> }>();
|
|
96
|
+
|
|
97
|
+
function loadZaloWebhookModule(): Promise<ZaloWebhookModule> {
|
|
98
|
+
zaloWebhookModulePromise ??= import("./monitor.webhook.js");
|
|
99
|
+
return zaloWebhookModulePromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function registerSharedHostedMediaRoute(params: {
|
|
103
|
+
path: string;
|
|
104
|
+
accountId: string;
|
|
105
|
+
log?: (message: string) => void;
|
|
106
|
+
}): () => void {
|
|
107
|
+
const unregister = registerPluginHttpRoute({
|
|
108
|
+
auth: "plugin",
|
|
109
|
+
match: "prefix",
|
|
110
|
+
path: params.path,
|
|
111
|
+
pluginId: "zalo",
|
|
112
|
+
source: "zalo-hosted-media",
|
|
113
|
+
accountId: params.accountId,
|
|
114
|
+
log: params.log,
|
|
115
|
+
handler: async (req, res) => {
|
|
116
|
+
const handled = await tryHandleHostedZaloMediaRequest(req, res);
|
|
117
|
+
if (!handled && !res.headersSent) {
|
|
118
|
+
res.statusCode = 404;
|
|
119
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
120
|
+
res.end("Not Found");
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const existing = hostedMediaRouteRefs.get(params.path);
|
|
126
|
+
if (existing) {
|
|
127
|
+
existing.count += 1;
|
|
128
|
+
existing.unregisters.push(unregister);
|
|
129
|
+
return () => {
|
|
130
|
+
const current = hostedMediaRouteRefs.get(params.path);
|
|
131
|
+
if (!current) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (current.count > 1) {
|
|
135
|
+
current.count -= 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
hostedMediaRouteRefs.delete(params.path);
|
|
139
|
+
for (const unregisterHandle of current.unregisters) {
|
|
140
|
+
unregisterHandle();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
hostedMediaRouteRefs.set(params.path, { count: 1, unregisters: [unregister] });
|
|
146
|
+
return () => {
|
|
147
|
+
const current = hostedMediaRouteRefs.get(params.path);
|
|
148
|
+
if (!current) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (current.count > 1) {
|
|
152
|
+
current.count -= 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
hostedMediaRouteRefs.delete(params.path);
|
|
156
|
+
for (const unregisterHandle of current.unregisters) {
|
|
157
|
+
unregisterHandle();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type ZaloMessagePipelineParams = ZaloProcessingContext & {
|
|
163
|
+
message: ZaloMessage;
|
|
164
|
+
text?: string;
|
|
165
|
+
mediaPath?: string;
|
|
166
|
+
mediaType?: string;
|
|
167
|
+
authorization?: ZaloMessageAuthorizationResult;
|
|
168
|
+
};
|
|
169
|
+
type ZaloImageMessageParams = ZaloProcessingContext & {
|
|
170
|
+
message: ZaloMessage;
|
|
171
|
+
};
|
|
172
|
+
type ZaloMessageAuthorizationResult = {
|
|
173
|
+
chatId: string;
|
|
174
|
+
commandAuthorized: boolean | undefined;
|
|
175
|
+
isGroup: boolean;
|
|
176
|
+
rawBody: string;
|
|
177
|
+
senderId: string;
|
|
178
|
+
senderName: string | undefined;
|
|
179
|
+
};
|
|
78
180
|
|
|
79
181
|
function formatZaloError(error: unknown): string {
|
|
80
182
|
if (error instanceof Error) {
|
|
@@ -103,100 +205,79 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
|
|
|
103
205
|
}
|
|
104
206
|
}
|
|
105
207
|
|
|
106
|
-
export function registerZaloWebhookTarget(target: ZaloWebhookTarget): () => void {
|
|
107
|
-
return registerZaloWebhookTargetInternal(target, {
|
|
108
|
-
route: {
|
|
109
|
-
auth: "plugin",
|
|
110
|
-
match: "exact",
|
|
111
|
-
pluginId: "zalo",
|
|
112
|
-
source: "zalo-webhook",
|
|
113
|
-
accountId: target.account.accountId,
|
|
114
|
-
log: target.runtime.log,
|
|
115
|
-
handler: async (req, res) => {
|
|
116
|
-
const handled = await handleZaloWebhookRequest(req, res);
|
|
117
|
-
if (!handled && !res.headersSent) {
|
|
118
|
-
res.statusCode = 404;
|
|
119
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
120
|
-
res.end("Not Found");
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export {
|
|
128
|
-
clearZaloWebhookSecurityStateForTest,
|
|
129
|
-
getZaloWebhookRateLimitStateSizeForTest,
|
|
130
|
-
getZaloWebhookStatusCounterSizeForTest,
|
|
131
|
-
};
|
|
132
|
-
|
|
133
208
|
export async function handleZaloWebhookRequest(
|
|
134
209
|
req: IncomingMessage,
|
|
135
210
|
res: ServerResponse,
|
|
136
211
|
): Promise<boolean> {
|
|
137
|
-
|
|
138
|
-
await
|
|
212
|
+
const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } =
|
|
213
|
+
await loadZaloWebhookModule();
|
|
214
|
+
return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
|
|
215
|
+
await processUpdate({
|
|
139
216
|
update,
|
|
140
|
-
target.token,
|
|
141
|
-
target.account,
|
|
142
|
-
target.config,
|
|
143
|
-
target.runtime,
|
|
144
|
-
target.core as ZaloCoreRuntime,
|
|
145
|
-
target.mediaMaxMb,
|
|
146
|
-
target.
|
|
147
|
-
target.
|
|
148
|
-
|
|
217
|
+
token: target.token,
|
|
218
|
+
account: target.account,
|
|
219
|
+
config: target.config,
|
|
220
|
+
runtime: target.runtime,
|
|
221
|
+
core: target.core as ZaloCoreRuntime,
|
|
222
|
+
mediaMaxMb: target.mediaMaxMb,
|
|
223
|
+
canHostMedia: target.canHostMedia,
|
|
224
|
+
webhookUrl: target.webhookUrl,
|
|
225
|
+
webhookPath: target.webhookPath,
|
|
226
|
+
statusSink: target.statusSink,
|
|
227
|
+
fetcher: target.fetcher,
|
|
228
|
+
});
|
|
149
229
|
});
|
|
150
230
|
}
|
|
151
231
|
|
|
152
|
-
function startPollingLoop(params: {
|
|
153
|
-
token: string;
|
|
154
|
-
account: ResolvedZaloAccount;
|
|
155
|
-
config: OpenClawConfig;
|
|
156
|
-
runtime: ZaloRuntimeEnv;
|
|
157
|
-
core: ZaloCoreRuntime;
|
|
158
|
-
abortSignal: AbortSignal;
|
|
159
|
-
isStopped: () => boolean;
|
|
160
|
-
mediaMaxMb: number;
|
|
161
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
162
|
-
fetcher?: ZaloFetch;
|
|
163
|
-
}) {
|
|
232
|
+
function startPollingLoop(params: ZaloPollingLoopParams) {
|
|
164
233
|
const {
|
|
165
234
|
token,
|
|
166
235
|
account,
|
|
167
236
|
config,
|
|
168
237
|
runtime,
|
|
169
238
|
core,
|
|
239
|
+
mediaMaxMb,
|
|
240
|
+
canHostMedia,
|
|
241
|
+
webhookUrl,
|
|
242
|
+
webhookPath,
|
|
170
243
|
abortSignal,
|
|
171
244
|
isStopped,
|
|
172
|
-
mediaMaxMb,
|
|
173
245
|
statusSink,
|
|
174
246
|
fetcher,
|
|
175
247
|
} = params;
|
|
176
248
|
const pollTimeout = 30;
|
|
249
|
+
const processingContext = {
|
|
250
|
+
token,
|
|
251
|
+
account,
|
|
252
|
+
config,
|
|
253
|
+
runtime,
|
|
254
|
+
core,
|
|
255
|
+
mediaMaxMb,
|
|
256
|
+
canHostMedia,
|
|
257
|
+
webhookUrl,
|
|
258
|
+
webhookPath,
|
|
259
|
+
statusSink,
|
|
260
|
+
fetcher,
|
|
261
|
+
};
|
|
177
262
|
|
|
178
263
|
runtime.log?.(`[${account.accountId}] Zalo polling loop started timeout=${String(pollTimeout)}s`);
|
|
179
264
|
|
|
180
|
-
const poll = async () => {
|
|
265
|
+
const poll = async (): Promise<void> => {
|
|
181
266
|
if (isStopped() || abortSignal.aborted) {
|
|
182
|
-
return;
|
|
267
|
+
return undefined;
|
|
183
268
|
}
|
|
184
269
|
|
|
185
270
|
try {
|
|
186
271
|
const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
|
|
272
|
+
if (isStopped() || abortSignal.aborted) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
187
275
|
if (response.ok && response.result) {
|
|
188
276
|
statusSink?.({ lastInboundAt: Date.now() });
|
|
189
|
-
await processUpdate(
|
|
190
|
-
response.result,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
config,
|
|
194
|
-
runtime,
|
|
195
|
-
core,
|
|
196
|
-
mediaMaxMb,
|
|
197
|
-
statusSink,
|
|
198
|
-
fetcher,
|
|
199
|
-
);
|
|
277
|
+
await processUpdate({
|
|
278
|
+
update: response.result,
|
|
279
|
+
...processingContext,
|
|
280
|
+
});
|
|
200
281
|
}
|
|
201
282
|
} catch (err) {
|
|
202
283
|
if (err instanceof ZaloApiError && err.isPollingTimeout) {
|
|
@@ -215,38 +296,39 @@ function startPollingLoop(params: {
|
|
|
215
296
|
void poll();
|
|
216
297
|
}
|
|
217
298
|
|
|
218
|
-
async function processUpdate(
|
|
219
|
-
update
|
|
220
|
-
token: string,
|
|
221
|
-
account: ResolvedZaloAccount,
|
|
222
|
-
config: OpenClawConfig,
|
|
223
|
-
runtime: ZaloRuntimeEnv,
|
|
224
|
-
core: ZaloCoreRuntime,
|
|
225
|
-
mediaMaxMb: number,
|
|
226
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
227
|
-
fetcher?: ZaloFetch,
|
|
228
|
-
): Promise<void> {
|
|
299
|
+
async function processUpdate(params: ZaloUpdateProcessingParams): Promise<void> {
|
|
300
|
+
const { update, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher } = params;
|
|
229
301
|
const { event_name, message } = update;
|
|
302
|
+
const sharedContext = {
|
|
303
|
+
token,
|
|
304
|
+
account,
|
|
305
|
+
config,
|
|
306
|
+
runtime,
|
|
307
|
+
core,
|
|
308
|
+
mediaMaxMb,
|
|
309
|
+
canHostMedia: params.canHostMedia,
|
|
310
|
+
webhookUrl: params.webhookUrl,
|
|
311
|
+
webhookPath: params.webhookPath,
|
|
312
|
+
statusSink,
|
|
313
|
+
fetcher,
|
|
314
|
+
};
|
|
230
315
|
if (!message) {
|
|
231
|
-
return;
|
|
316
|
+
return undefined;
|
|
232
317
|
}
|
|
233
318
|
|
|
234
319
|
switch (event_name) {
|
|
235
320
|
case "message.text.received":
|
|
236
|
-
await handleTextMessage(
|
|
321
|
+
await handleTextMessage({
|
|
322
|
+
message,
|
|
323
|
+
...sharedContext,
|
|
324
|
+
});
|
|
237
325
|
break;
|
|
238
326
|
case "message.image.received":
|
|
239
|
-
await handleImageMessage(
|
|
327
|
+
await handleImageMessage({
|
|
240
328
|
message,
|
|
241
|
-
|
|
242
|
-
account,
|
|
243
|
-
config,
|
|
244
|
-
runtime,
|
|
245
|
-
core,
|
|
329
|
+
...sharedContext,
|
|
246
330
|
mediaMaxMb,
|
|
247
|
-
|
|
248
|
-
fetcher,
|
|
249
|
-
);
|
|
331
|
+
});
|
|
250
332
|
break;
|
|
251
333
|
case "message.sticker.received":
|
|
252
334
|
logVerbose(core, runtime, `[${account.accountId}] Received sticker from ${message.from.id}`);
|
|
@@ -262,55 +344,43 @@ async function processUpdate(
|
|
|
262
344
|
}
|
|
263
345
|
|
|
264
346
|
async function handleTextMessage(
|
|
265
|
-
message: ZaloMessage,
|
|
266
|
-
token: string,
|
|
267
|
-
account: ResolvedZaloAccount,
|
|
268
|
-
config: OpenClawConfig,
|
|
269
|
-
runtime: ZaloRuntimeEnv,
|
|
270
|
-
core: ZaloCoreRuntime,
|
|
271
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
272
|
-
fetcher?: ZaloFetch,
|
|
347
|
+
params: ZaloProcessingContext & { message: ZaloMessage },
|
|
273
348
|
): Promise<void> {
|
|
349
|
+
const { message } = params;
|
|
274
350
|
const { text } = message;
|
|
275
351
|
if (!text?.trim()) {
|
|
276
|
-
return;
|
|
352
|
+
return undefined;
|
|
277
353
|
}
|
|
278
354
|
|
|
279
355
|
await processMessageWithPipeline({
|
|
280
|
-
|
|
281
|
-
token,
|
|
282
|
-
account,
|
|
283
|
-
config,
|
|
284
|
-
runtime,
|
|
285
|
-
core,
|
|
356
|
+
...params,
|
|
286
357
|
text,
|
|
287
358
|
mediaPath: undefined,
|
|
288
359
|
mediaType: undefined,
|
|
289
|
-
statusSink,
|
|
290
|
-
fetcher,
|
|
291
360
|
});
|
|
292
361
|
}
|
|
293
362
|
|
|
294
|
-
async function handleImageMessage(
|
|
295
|
-
message
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
)
|
|
305
|
-
|
|
363
|
+
async function handleImageMessage(params: ZaloImageMessageParams): Promise<void> {
|
|
364
|
+
const { message, mediaMaxMb, account, core, runtime } = params;
|
|
365
|
+
const { photo_url, caption } = message;
|
|
366
|
+
const authorization = await authorizeZaloMessage({
|
|
367
|
+
...params,
|
|
368
|
+
text: caption,
|
|
369
|
+
// Use a sentinel so auth sees this as an inbound image before the download happens.
|
|
370
|
+
mediaPath: photo_url ? "__pending_media__" : undefined,
|
|
371
|
+
mediaType: undefined,
|
|
372
|
+
});
|
|
373
|
+
if (!authorization) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
306
376
|
|
|
307
377
|
let mediaPath: string | undefined;
|
|
308
378
|
let mediaType: string | undefined;
|
|
309
379
|
|
|
310
|
-
if (
|
|
380
|
+
if (photo_url) {
|
|
311
381
|
try {
|
|
312
382
|
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
313
|
-
const fetched = await core.channel.media.fetchRemoteMedia({ url:
|
|
383
|
+
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo_url, maxBytes });
|
|
314
384
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
315
385
|
fetched.buffer,
|
|
316
386
|
fetched.contentType,
|
|
@@ -325,57 +395,30 @@ async function handleImageMessage(
|
|
|
325
395
|
}
|
|
326
396
|
|
|
327
397
|
await processMessageWithPipeline({
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
account,
|
|
331
|
-
config,
|
|
332
|
-
runtime,
|
|
333
|
-
core,
|
|
398
|
+
...params,
|
|
399
|
+
authorization,
|
|
334
400
|
text: caption,
|
|
335
401
|
mediaPath,
|
|
336
402
|
mediaType,
|
|
337
|
-
statusSink,
|
|
338
|
-
fetcher,
|
|
339
403
|
});
|
|
340
404
|
}
|
|
341
405
|
|
|
342
|
-
async function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
account
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
core: ZaloCoreRuntime;
|
|
349
|
-
text?: string;
|
|
350
|
-
mediaPath?: string;
|
|
351
|
-
mediaType?: string;
|
|
352
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
353
|
-
fetcher?: ZaloFetch;
|
|
354
|
-
}): Promise<void> {
|
|
355
|
-
const {
|
|
356
|
-
message,
|
|
357
|
-
token,
|
|
358
|
-
account,
|
|
359
|
-
config,
|
|
360
|
-
runtime,
|
|
361
|
-
core,
|
|
362
|
-
text,
|
|
363
|
-
mediaPath,
|
|
364
|
-
mediaType,
|
|
365
|
-
statusSink,
|
|
366
|
-
fetcher,
|
|
367
|
-
} = params;
|
|
368
|
-
const pairing = createScopedPairingAccess({
|
|
406
|
+
async function authorizeZaloMessage(
|
|
407
|
+
params: ZaloMessagePipelineParams,
|
|
408
|
+
): Promise<ZaloMessageAuthorizationResult | undefined> {
|
|
409
|
+
const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } =
|
|
410
|
+
params;
|
|
411
|
+
const pairing = createChannelPairingController({
|
|
369
412
|
core,
|
|
370
413
|
channel: "zalo",
|
|
371
414
|
accountId: account.accountId,
|
|
372
415
|
});
|
|
373
|
-
const { from, chat
|
|
416
|
+
const { from, chat } = message;
|
|
374
417
|
|
|
375
418
|
const isGroup = chat.chat_type === "GROUP";
|
|
376
419
|
const chatId = chat.id;
|
|
377
420
|
const senderId = from.id;
|
|
378
|
-
const senderName = from.name;
|
|
421
|
+
const senderName = from.display_name ?? from.name;
|
|
379
422
|
|
|
380
423
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
381
424
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
@@ -411,7 +454,7 @@ async function processMessageWithPipeline(params: {
|
|
|
411
454
|
} else if (groupAccess.reason === "sender_not_allowlisted") {
|
|
412
455
|
logVerbose(core, runtime, `zalo: drop group sender ${senderId} (groupPolicy=allowlist)`);
|
|
413
456
|
}
|
|
414
|
-
return;
|
|
457
|
+
return undefined;
|
|
415
458
|
}
|
|
416
459
|
}
|
|
417
460
|
|
|
@@ -426,6 +469,8 @@ async function processMessageWithPipeline(params: {
|
|
|
426
469
|
configuredGroupAllowFrom: groupAllowFrom,
|
|
427
470
|
senderId,
|
|
428
471
|
isSenderAllowed: isZaloSenderAllowed,
|
|
472
|
+
channel: "zalo",
|
|
473
|
+
accountId: account.accountId,
|
|
429
474
|
readAllowFromStore: pairing.readAllowFromStore,
|
|
430
475
|
runtime: core.channel.commands,
|
|
431
476
|
});
|
|
@@ -437,16 +482,14 @@ async function processMessageWithPipeline(params: {
|
|
|
437
482
|
});
|
|
438
483
|
if (directDmOutcome === "disabled") {
|
|
439
484
|
logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
|
|
440
|
-
return;
|
|
485
|
+
return undefined;
|
|
441
486
|
}
|
|
442
487
|
if (directDmOutcome === "unauthorized") {
|
|
443
488
|
if (dmPolicy === "pairing") {
|
|
444
|
-
await
|
|
445
|
-
channel: "zalo",
|
|
489
|
+
await pairing.issueChallenge({
|
|
446
490
|
senderId,
|
|
447
491
|
senderIdLine: `Your Zalo user id: ${senderId}`,
|
|
448
492
|
meta: { name: senderName ?? undefined },
|
|
449
|
-
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
450
493
|
onCreated: () => {
|
|
451
494
|
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
|
452
495
|
},
|
|
@@ -472,8 +515,45 @@ async function processMessageWithPipeline(params: {
|
|
|
472
515
|
`Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
473
516
|
);
|
|
474
517
|
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
chatId,
|
|
523
|
+
commandAuthorized,
|
|
524
|
+
isGroup,
|
|
525
|
+
rawBody,
|
|
526
|
+
senderId,
|
|
527
|
+
senderName,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Promise<void> {
|
|
532
|
+
const {
|
|
533
|
+
message,
|
|
534
|
+
token,
|
|
535
|
+
account,
|
|
536
|
+
config,
|
|
537
|
+
runtime,
|
|
538
|
+
core,
|
|
539
|
+
mediaPath,
|
|
540
|
+
mediaType,
|
|
541
|
+
statusSink,
|
|
542
|
+
fetcher,
|
|
543
|
+
authorization: authorizationOverride,
|
|
544
|
+
} = params;
|
|
545
|
+
const { message_id, date } = message;
|
|
546
|
+
const authorization =
|
|
547
|
+
authorizationOverride ??
|
|
548
|
+
(await authorizeZaloMessage({
|
|
549
|
+
...params,
|
|
550
|
+
mediaPath,
|
|
551
|
+
mediaType,
|
|
552
|
+
}));
|
|
553
|
+
if (!authorization) {
|
|
475
554
|
return;
|
|
476
555
|
}
|
|
556
|
+
const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization;
|
|
477
557
|
|
|
478
558
|
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
479
559
|
cfg: config,
|
|
@@ -504,36 +584,54 @@ async function processMessageWithPipeline(params: {
|
|
|
504
584
|
body: rawBody,
|
|
505
585
|
});
|
|
506
586
|
|
|
507
|
-
const ctxPayload = core.channel.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
587
|
+
const ctxPayload = core.channel.turn.buildContext({
|
|
588
|
+
channel: "zalo",
|
|
589
|
+
accountId: route.accountId,
|
|
590
|
+
messageId: message_id,
|
|
591
|
+
timestamp: date ? date * 1000 : undefined,
|
|
592
|
+
from: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
|
|
593
|
+
sender: {
|
|
594
|
+
id: senderId,
|
|
595
|
+
name: senderName || undefined,
|
|
596
|
+
},
|
|
597
|
+
conversation: {
|
|
598
|
+
kind: isGroup ? "group" : "direct",
|
|
599
|
+
id: chatId,
|
|
600
|
+
label: fromLabel,
|
|
601
|
+
routePeer: {
|
|
602
|
+
kind: isGroup ? "group" : "direct",
|
|
603
|
+
id: chatId,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
route: {
|
|
607
|
+
agentId: route.agentId,
|
|
608
|
+
accountId: route.accountId,
|
|
609
|
+
routeSessionKey: route.sessionKey,
|
|
610
|
+
},
|
|
611
|
+
reply: {
|
|
612
|
+
to: `zalo:${chatId}`,
|
|
613
|
+
originatingTo: `zalo:${chatId}`,
|
|
614
|
+
},
|
|
615
|
+
message: {
|
|
616
|
+
body,
|
|
617
|
+
bodyForAgent: rawBody,
|
|
618
|
+
rawBody,
|
|
619
|
+
commandBody: rawBody,
|
|
620
|
+
envelopeFrom: fromLabel,
|
|
621
|
+
},
|
|
622
|
+
media:
|
|
623
|
+
mediaPath || mediaType
|
|
624
|
+
? [
|
|
625
|
+
{
|
|
626
|
+
path: mediaPath,
|
|
627
|
+
url: mediaPath,
|
|
628
|
+
contentType: mediaType,
|
|
629
|
+
},
|
|
630
|
+
]
|
|
631
|
+
: undefined,
|
|
632
|
+
extra: {
|
|
633
|
+
CommandAuthorized: commandAuthorized,
|
|
634
|
+
GroupSubject: undefined,
|
|
537
635
|
},
|
|
538
636
|
});
|
|
539
637
|
|
|
@@ -542,61 +640,95 @@ async function processMessageWithPipeline(params: {
|
|
|
542
640
|
channel: "zalo",
|
|
543
641
|
accountId: account.accountId,
|
|
544
642
|
});
|
|
545
|
-
const { onModelSelected, ...
|
|
643
|
+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
|
546
644
|
cfg: config,
|
|
547
645
|
agentId: route.agentId,
|
|
548
646
|
channel: "zalo",
|
|
549
647
|
accountId: account.accountId,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
await sendChatAction(
|
|
554
|
-
token,
|
|
555
|
-
{
|
|
556
|
-
chat_id: chatId,
|
|
557
|
-
action: "typing",
|
|
558
|
-
},
|
|
559
|
-
fetcher,
|
|
560
|
-
ZALO_TYPING_TIMEOUT_MS,
|
|
561
|
-
);
|
|
562
|
-
},
|
|
563
|
-
onStartError: (err) => {
|
|
564
|
-
logTypingFailure({
|
|
565
|
-
log: (message) => logVerbose(core, runtime, message),
|
|
566
|
-
channel: "zalo",
|
|
567
|
-
action: "start",
|
|
568
|
-
target: chatId,
|
|
569
|
-
error: err,
|
|
570
|
-
});
|
|
571
|
-
},
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
575
|
-
ctx: ctxPayload,
|
|
576
|
-
cfg: config,
|
|
577
|
-
dispatcherOptions: {
|
|
578
|
-
...prefixOptions,
|
|
579
|
-
typingCallbacks,
|
|
580
|
-
deliver: async (payload) => {
|
|
581
|
-
await deliverZaloReply({
|
|
582
|
-
payload,
|
|
648
|
+
typing: {
|
|
649
|
+
start: async () => {
|
|
650
|
+
await sendChatAction(
|
|
583
651
|
token,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
accountId: account.accountId,
|
|
589
|
-
statusSink,
|
|
652
|
+
{
|
|
653
|
+
chat_id: chatId,
|
|
654
|
+
action: "typing",
|
|
655
|
+
},
|
|
590
656
|
fetcher,
|
|
591
|
-
|
|
592
|
-
|
|
657
|
+
ZALO_TYPING_TIMEOUT_MS,
|
|
658
|
+
);
|
|
593
659
|
},
|
|
594
|
-
|
|
595
|
-
|
|
660
|
+
onStartError: (err) => {
|
|
661
|
+
logTypingFailure({
|
|
662
|
+
log: (message) => logVerbose(core, runtime, message),
|
|
663
|
+
channel: "zalo",
|
|
664
|
+
action: "start",
|
|
665
|
+
target: chatId,
|
|
666
|
+
error: err,
|
|
667
|
+
});
|
|
596
668
|
},
|
|
597
669
|
},
|
|
598
|
-
|
|
599
|
-
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await core.channel.turn.run({
|
|
673
|
+
channel: "zalo",
|
|
674
|
+
accountId: account.accountId,
|
|
675
|
+
raw: message,
|
|
676
|
+
adapter: {
|
|
677
|
+
ingest: () => ({
|
|
678
|
+
id: message_id,
|
|
679
|
+
timestamp: date ? date * 1000 : undefined,
|
|
680
|
+
rawText: rawBody,
|
|
681
|
+
textForAgent: rawBody,
|
|
682
|
+
textForCommands: rawBody,
|
|
683
|
+
raw: message,
|
|
684
|
+
}),
|
|
685
|
+
resolveTurn: () => ({
|
|
686
|
+
cfg: config,
|
|
687
|
+
channel: "zalo",
|
|
688
|
+
accountId: account.accountId,
|
|
689
|
+
agentId: route.agentId,
|
|
690
|
+
routeSessionKey: route.sessionKey,
|
|
691
|
+
storePath,
|
|
692
|
+
ctxPayload,
|
|
693
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
694
|
+
dispatchReplyWithBufferedBlockDispatcher:
|
|
695
|
+
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
696
|
+
delivery: {
|
|
697
|
+
deliver: async (payload) => {
|
|
698
|
+
await deliverZaloReply({
|
|
699
|
+
payload,
|
|
700
|
+
token,
|
|
701
|
+
chatId,
|
|
702
|
+
runtime,
|
|
703
|
+
core,
|
|
704
|
+
config,
|
|
705
|
+
webhookUrl: params.webhookUrl,
|
|
706
|
+
webhookPath: params.webhookPath,
|
|
707
|
+
proxyUrl: account.config.proxy,
|
|
708
|
+
mediaMaxBytes: params.mediaMaxMb * 1024 * 1024,
|
|
709
|
+
canHostMedia: params.canHostMedia,
|
|
710
|
+
accountId: account.accountId,
|
|
711
|
+
statusSink,
|
|
712
|
+
fetcher,
|
|
713
|
+
tableMode,
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
onError: (err, info) => {
|
|
717
|
+
runtime.error?.(
|
|
718
|
+
`[${account.accountId}] Zalo ${info.kind} reply failed: ${String(err)}`,
|
|
719
|
+
);
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
dispatcherOptions: replyPipeline,
|
|
723
|
+
replyOptions: {
|
|
724
|
+
onModelSelected,
|
|
725
|
+
},
|
|
726
|
+
record: {
|
|
727
|
+
onRecordError: (err) => {
|
|
728
|
+
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
}),
|
|
600
732
|
},
|
|
601
733
|
});
|
|
602
734
|
}
|
|
@@ -608,41 +740,70 @@ async function deliverZaloReply(params: {
|
|
|
608
740
|
runtime: ZaloRuntimeEnv;
|
|
609
741
|
core: ZaloCoreRuntime;
|
|
610
742
|
config: OpenClawConfig;
|
|
743
|
+
webhookUrl?: string;
|
|
744
|
+
webhookPath?: string;
|
|
745
|
+
proxyUrl?: string;
|
|
746
|
+
mediaMaxBytes: number;
|
|
747
|
+
canHostMedia: boolean;
|
|
611
748
|
accountId?: string;
|
|
612
|
-
statusSink?:
|
|
749
|
+
statusSink?: ZaloStatusSink;
|
|
613
750
|
fetcher?: ZaloFetch;
|
|
614
751
|
tableMode?: MarkdownTableMode;
|
|
615
752
|
}): Promise<void> {
|
|
616
|
-
const {
|
|
753
|
+
const {
|
|
754
|
+
payload,
|
|
755
|
+
token,
|
|
756
|
+
chatId,
|
|
757
|
+
runtime,
|
|
758
|
+
core,
|
|
759
|
+
config,
|
|
760
|
+
webhookUrl,
|
|
761
|
+
webhookPath,
|
|
762
|
+
proxyUrl,
|
|
763
|
+
mediaMaxBytes,
|
|
764
|
+
canHostMedia,
|
|
765
|
+
accountId,
|
|
766
|
+
statusSink,
|
|
767
|
+
fetcher,
|
|
768
|
+
} = params;
|
|
617
769
|
const tableMode = params.tableMode ?? "code";
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
mediaUrls: resolveOutboundMediaUrls(payload),
|
|
621
|
-
caption: text,
|
|
622
|
-
send: async ({ mediaUrl, caption }) => {
|
|
623
|
-
await sendPhoto(token, { chat_id: chatId, photo: mediaUrl, caption }, fetcher);
|
|
624
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
625
|
-
},
|
|
626
|
-
onError: (error) => {
|
|
627
|
-
runtime.error?.(`Zalo photo send failed: ${String(error)}`);
|
|
628
|
-
},
|
|
770
|
+
const reply = resolveSendableOutboundReplyParts(payload, {
|
|
771
|
+
text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
|
|
629
772
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
for (const chunk of chunks) {
|
|
773
|
+
const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
|
|
774
|
+
await deliverTextOrMediaReply({
|
|
775
|
+
payload,
|
|
776
|
+
text: reply.text,
|
|
777
|
+
chunkText: (value) =>
|
|
778
|
+
core.channel.text.chunkMarkdownTextWithMode(value, ZALO_TEXT_LIMIT, chunkMode),
|
|
779
|
+
sendText: async (chunk) => {
|
|
638
780
|
try {
|
|
639
781
|
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
|
640
782
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
641
783
|
} catch (err) {
|
|
642
784
|
runtime.error?.(`Zalo message send failed: ${String(err)}`);
|
|
643
785
|
}
|
|
644
|
-
}
|
|
645
|
-
|
|
786
|
+
},
|
|
787
|
+
sendMedia: async ({ mediaUrl, caption }) => {
|
|
788
|
+
const sendableMediaUrl =
|
|
789
|
+
canHostMedia && webhookUrl && webhookPath
|
|
790
|
+
? await prepareHostedZaloMediaUrl({
|
|
791
|
+
mediaUrl,
|
|
792
|
+
webhookUrl,
|
|
793
|
+
webhookPath,
|
|
794
|
+
maxBytes: mediaMaxBytes,
|
|
795
|
+
proxyUrl,
|
|
796
|
+
})
|
|
797
|
+
: mediaUrl;
|
|
798
|
+
await sendPhoto(token, { chat_id: chatId, photo: sendableMediaUrl, caption }, fetcher);
|
|
799
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
800
|
+
},
|
|
801
|
+
onMediaError: (error) => {
|
|
802
|
+
runtime.error?.(
|
|
803
|
+
`Zalo photo send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
804
|
+
);
|
|
805
|
+
},
|
|
806
|
+
});
|
|
646
807
|
}
|
|
647
808
|
|
|
648
809
|
export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<void> {
|
|
@@ -664,6 +825,23 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
664
825
|
const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
|
665
826
|
const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
|
|
666
827
|
const mode = useWebhook ? "webhook" : "polling";
|
|
828
|
+
const effectiveWebhookUrl = normalizeWebhookUrl(webhookUrl ?? account.config.webhookUrl);
|
|
829
|
+
const effectiveWebhookPath =
|
|
830
|
+
effectiveWebhookUrl || webhookPath?.trim() || account.config.webhookPath?.trim()
|
|
831
|
+
? (resolveWebhookPath({
|
|
832
|
+
webhookPath: webhookPath ?? account.config.webhookPath,
|
|
833
|
+
webhookUrl: effectiveWebhookUrl,
|
|
834
|
+
defaultPath: null,
|
|
835
|
+
}) ?? undefined)
|
|
836
|
+
: undefined;
|
|
837
|
+
const canHostMedia = Boolean(effectiveWebhookUrl && effectiveWebhookPath);
|
|
838
|
+
const hostedMediaRoutePath =
|
|
839
|
+
canHostMedia && effectiveWebhookUrl
|
|
840
|
+
? resolveHostedZaloMediaRoutePrefix({
|
|
841
|
+
webhookUrl: effectiveWebhookUrl,
|
|
842
|
+
webhookPath: effectiveWebhookPath,
|
|
843
|
+
})
|
|
844
|
+
: undefined;
|
|
667
845
|
|
|
668
846
|
let stopped = false;
|
|
669
847
|
const stopHandlers: Array<() => void> = [];
|
|
@@ -678,32 +856,49 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
678
856
|
handler();
|
|
679
857
|
}
|
|
680
858
|
};
|
|
859
|
+
const stopOnAbort = () => {
|
|
860
|
+
if (!useWebhook) {
|
|
861
|
+
stop();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
abortSignal.addEventListener("abort", stopOnAbort, { once: true });
|
|
681
866
|
|
|
682
867
|
runtime.log?.(
|
|
683
868
|
`[${account.accountId}] Zalo provider init mode=${mode} mediaMaxMb=${String(effectiveMediaMaxMb)}`,
|
|
684
869
|
);
|
|
685
870
|
|
|
686
871
|
try {
|
|
872
|
+
if (hostedMediaRoutePath) {
|
|
873
|
+
const unregisterHostedMediaRoute = registerSharedHostedMediaRoute({
|
|
874
|
+
path: hostedMediaRoutePath,
|
|
875
|
+
accountId: account.accountId,
|
|
876
|
+
log: runtime.log,
|
|
877
|
+
});
|
|
878
|
+
stopHandlers.push(unregisterHostedMediaRoute);
|
|
879
|
+
}
|
|
880
|
+
|
|
687
881
|
if (useWebhook) {
|
|
688
|
-
|
|
882
|
+
const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
|
|
883
|
+
if (!effectiveWebhookUrl || !webhookSecret) {
|
|
689
884
|
throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
|
|
690
885
|
}
|
|
691
|
-
if (!
|
|
886
|
+
if (!effectiveWebhookUrl.startsWith("https://")) {
|
|
692
887
|
throw new Error("Zalo webhook URL must use HTTPS");
|
|
693
888
|
}
|
|
694
889
|
if (webhookSecret.length < 8 || webhookSecret.length > 256) {
|
|
695
890
|
throw new Error("Zalo webhook secret must be 8-256 characters");
|
|
696
891
|
}
|
|
697
892
|
|
|
698
|
-
const path =
|
|
893
|
+
const path = effectiveWebhookPath;
|
|
699
894
|
if (!path) {
|
|
700
895
|
throw new Error("Zalo webhookPath could not be derived");
|
|
701
896
|
}
|
|
702
897
|
|
|
703
898
|
runtime.log?.(
|
|
704
|
-
`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(
|
|
899
|
+
`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(effectiveWebhookUrl)}`,
|
|
705
900
|
);
|
|
706
|
-
await setWebhook(token, { url:
|
|
901
|
+
await setWebhook(token, { url: effectiveWebhookUrl, secret_token: webhookSecret }, fetcher);
|
|
707
902
|
let webhookCleanupPromise: Promise<void> | undefined;
|
|
708
903
|
cleanupWebhook = async () => {
|
|
709
904
|
if (!webhookCleanupPromise) {
|
|
@@ -725,18 +920,41 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
725
920
|
};
|
|
726
921
|
runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
|
|
727
922
|
|
|
728
|
-
const unregister = registerZaloWebhookTarget(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
923
|
+
const unregister = registerZaloWebhookTarget(
|
|
924
|
+
{
|
|
925
|
+
token,
|
|
926
|
+
account,
|
|
927
|
+
config,
|
|
928
|
+
runtime,
|
|
929
|
+
core,
|
|
930
|
+
path,
|
|
931
|
+
webhookUrl: effectiveWebhookUrl,
|
|
932
|
+
webhookPath: path,
|
|
933
|
+
secret: webhookSecret,
|
|
934
|
+
statusSink: (patch) => statusSink?.(patch),
|
|
935
|
+
mediaMaxMb: effectiveMediaMaxMb,
|
|
936
|
+
canHostMedia,
|
|
937
|
+
fetcher,
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
route: {
|
|
941
|
+
auth: "plugin",
|
|
942
|
+
match: "exact",
|
|
943
|
+
pluginId: "zalo",
|
|
944
|
+
source: "zalo-webhook",
|
|
945
|
+
accountId: account.accountId,
|
|
946
|
+
log: runtime.log,
|
|
947
|
+
handler: async (req, res) => {
|
|
948
|
+
const handled = await handleZaloWebhookRequest(req, res);
|
|
949
|
+
if (!handled && !res.headersSent) {
|
|
950
|
+
res.statusCode = 404;
|
|
951
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
952
|
+
res.end("Not Found");
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
);
|
|
740
958
|
stopHandlers.push(unregister);
|
|
741
959
|
await waitForAbortSignal(abortSignal);
|
|
742
960
|
return;
|
|
@@ -779,6 +997,9 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
779
997
|
config,
|
|
780
998
|
runtime,
|
|
781
999
|
core,
|
|
1000
|
+
canHostMedia,
|
|
1001
|
+
webhookUrl: effectiveWebhookUrl,
|
|
1002
|
+
webhookPath: effectiveWebhookPath,
|
|
782
1003
|
abortSignal,
|
|
783
1004
|
isStopped: () => stopped,
|
|
784
1005
|
mediaMaxMb: effectiveMediaMaxMb,
|
|
@@ -793,6 +1014,7 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
793
1014
|
);
|
|
794
1015
|
throw err;
|
|
795
1016
|
} finally {
|
|
1017
|
+
abortSignal.removeEventListener("abort", stopOnAbort);
|
|
796
1018
|
await cleanupWebhook?.();
|
|
797
1019
|
stop();
|
|
798
1020
|
runtime.log?.(`[${account.accountId}] Zalo provider stopped mode=${mode}`);
|
|
@@ -802,4 +1024,5 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
802
1024
|
export const __testing = {
|
|
803
1025
|
evaluateZaloGroupAccess,
|
|
804
1026
|
resolveZaloRuntimeGroupPolicy,
|
|
1027
|
+
clearHostedMediaRouteRefsForTest: () => hostedMediaRouteRefs.clear(),
|
|
805
1028
|
};
|