@jackle.dev/zalox-plugin 1.0.24 → 1.0.25
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/package.json +1 -1
- package/src/listener.ts +9 -8
package/package.json
CHANGED
package/src/listener.ts
CHANGED
|
@@ -62,7 +62,6 @@ async function downloadZaloMedia(url: string, profile: string, filename?: string
|
|
|
62
62
|
const credentialsPath = resolveCredentialsPath(profile);
|
|
63
63
|
const creds = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
|
|
64
64
|
|
|
65
|
-
// Zalo media URLs usually require cookies
|
|
66
65
|
const headers: Record<string, string> = {
|
|
67
66
|
'User-Agent': creds.userAgent || 'Mozilla/5.0',
|
|
68
67
|
'Cookie': typeof creds.cookie === 'object' ? JSON.stringify(creds.cookie) : String(creds.cookie),
|
|
@@ -77,7 +76,6 @@ async function downloadZaloMedia(url: string, profile: string, filename?: string
|
|
|
77
76
|
const contentType = res.headers.get('content-type');
|
|
78
77
|
const buffer = await res.arrayBuffer();
|
|
79
78
|
|
|
80
|
-
// Determine extension
|
|
81
79
|
let ext = extname(url).split('?')[0];
|
|
82
80
|
if (!ext || ext.length > 5) {
|
|
83
81
|
if (contentType?.includes('jpeg')) ext = '.jpg';
|
|
@@ -119,13 +117,11 @@ async function processMessage(
|
|
|
119
117
|
const core = getRuntime();
|
|
120
118
|
const { threadId, content, msgType, timestamp, isGroup, senderId, senderName, groupName, mediaUrl } = message;
|
|
121
119
|
|
|
122
|
-
// Allow empty content if it's an image
|
|
123
120
|
if (!content?.trim() && !mediaUrl) return;
|
|
124
121
|
|
|
125
122
|
const chatId = threadId;
|
|
126
123
|
const rawBody = content.trim();
|
|
127
124
|
|
|
128
|
-
// Handle media download
|
|
129
125
|
let mediaPath: string | undefined;
|
|
130
126
|
if (mediaUrl) {
|
|
131
127
|
const downloaded = await downloadZaloMedia(mediaUrl, profile);
|
|
@@ -137,7 +133,6 @@ async function processMessage(
|
|
|
137
133
|
}
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
// DM policy check
|
|
141
136
|
const configAllowFrom = (account.config.allowFrom ?? []).map(String);
|
|
142
137
|
const dmPolicy = account.config.dmPolicy ?? 'pairing';
|
|
143
138
|
|
|
@@ -153,8 +148,7 @@ async function processMessage(
|
|
|
153
148
|
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
154
149
|
useAccessGroups,
|
|
155
150
|
authorizers: [
|
|
156
|
-
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
157
|
-
],
|
|
151
|
+
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },\n ],
|
|
158
152
|
})
|
|
159
153
|
: undefined;
|
|
160
154
|
|
|
@@ -169,4 +163,11 @@ async function processMessage(
|
|
|
169
163
|
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
170
164
|
channel: 'zalox',
|
|
171
165
|
id: senderId,
|
|
172
|
-
meta: { name: senderName || undefined }
|
|
166
|
+
meta: { name: senderName || undefined },
|
|
167
|
+
});
|
|
168
|
+
if (created) {
|
|
169
|
+
runtime.log?.(`[zalox] pairing request sender=${senderId}`);
|
|
170
|
+
try {
|
|
171
|
+
await sendText(
|
|
172
|
+
chatId,
|
|
173
|
+
core.channel.pairing.buildPairingReply({\n channel: 'zalox',\n idLine: `Your Zalo user id: ${senderId}`,\n code,\n }),\n { profile },\n );\n statusSink?.({ lastOutboundAt: Date.now() });\n } catch (err) {\n runtime.error?.(`[zalox] pairing reply failed: ${String(err)}`);\n }\n }\n }\n return;\n }\n }\n }\n\n if (\n isGroup &&\n core.channel.commands.isControlCommandMessage(rawBody, config) &&\n commandAuthorized !== true\n ) {\n return;\n }\n\n const peer = isGroup\n ? { kind: 'group' as const, id: chatId }\n : { kind: 'dm' as const, id: senderId };\n\n const route = core.channel.routing.resolveAgentRoute({\n cfg: config,\n channel: 'zalox',\n accountId: account.accountId,\n peer,\n });\n\n const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;\n const storePath = core.channel.session.resolveStorePath(config.session?.store, {\n agentId: route.agentId,\n });\n const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);\n const previousTimestamp = core.channel.session.readSessionUpdatedAt({\n storePath,\n sessionKey: route.sessionKey,\n });\n const body = core.channel.reply.formatAgentEnvelope({\n channel: 'Zalo (ZaloX)',\n from: fromLabel,\n timestamp: timestamp ? timestamp * 1000 : undefined,\n previousTimestamp,\n envelope: envelopeOptions,\n body: rawBody || (mediaPath ? '[Image]' : ''),\n });\n\n const ctxPayload = core.channel.reply.finalizeInboundContext({\n Body: body,\n RawBody: rawBody,\n CommandBody: rawBody,\n From: isGroup ? `zalox:group:${chatId}` : `zalox:${senderId}`,\n To: `zalox:${chatId}`,\n SessionKey: route.sessionKey,\n AccountId: route.accountId,\n ChatType: isGroup ? 'group' : 'direct',\n ConversationLabel: fromLabel,\n SenderName: senderName || undefined,\n SenderId: senderId,\n CommandAuthorized: commandAuthorized,\n Provider: 'zalox',\n Surface: 'zalox',\n MessageSid: message.msgId ?? `${timestamp}`,\n OriginatingChannel: 'zalox',\n OriginatingTo: `zalox:${chatId}`,\n MediaPath: mediaPath,\n });\n\n await core.channel.session.recordInboundSession({\n storePath,\n sessionKey: ctxPayload.SessionKey ?? route.sessionKey,\n ctx: ctxPayload,\n onRecordError: (err: unknown) => {\n runtime.error?.(`[zalox] session meta error: ${String(err)}`);\n },\n });\n\n await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({\n ctx: ctxPayload,\n cfg: config,\n dispatcherOptions: {\n deliver: async (payload: any) => {\n const text = payload.text ?? '';\n if (!text) return;\n\n const tableMode = core.channel.text.resolveMarkdownTableMode({\n cfg: config,\n channel: 'zalox',\n accountId: account.accountId,\n });\n const converted = core.channel.text.convertMarkdownTables(text, tableMode ?? 'code');\n const chunkMode = core.channel.text.resolveChunkMode(config, 'zalox', account.accountId);\n const chunks = core.channel.text.chunkMarkdownTextWithMode(converted, 2000, chunkMode);\n\n for (const chunk of chunks) {\n try {\n await sendText(chatId, chunk, { profile, isGroup });\n statusSink?.({ lastOutboundAt: Date.now() });\n } catch (err) {\n runtime.error?.(`[zalox] send failed: ${String(err)}`);\n }\n }\n },\n onError: (err: unknown, info: any) => {\n runtime.error?.(`[${account.accountId}] ZaloX ${info.kind} reply failed: ${String(err)}`);\n },\n },\n });\n}\n\nexport async function startInProcessListener(\n options: ListenerOptions,\n): Promise<{ stop: () => void }> {\n const { account, config, runtime, api, ownId, profile, abortSignal, statusSink } = options;\n let stopped = false;\n\n const listener = api.listener;\n\n listener.on('message', (msg: any) => {\n if (stopped || abortSignal.aborted) return;\n\n const data = msg.data || msg;\n const isGroup = msg.type === 1;\n const senderId = data.uidFrom ? String(data.uidFrom) : '';\n \n if (senderId === ownId) return;\n\n const threadId = isGroup\n ? String(data.idTo || data.threadId || '')\n : senderId;\n\n const msgType = data.msgType || 0;\n \n let mediaUrl = undefined;\n mediaUrl = data.url || data.href;\n if (!mediaUrl && (msgType === 2 || String(msgType) === 'chat.photo')) {\n if (data.params?.url) mediaUrl = data.params.url;\n else if (typeof data.content === 'object' && data.content !== null) {\n mediaUrl = (data.content as any).href || (data.content as any).url || (data.content as any).thumb || (data.content as any).normalUrl;\n }\n else if (typeof data.content === 'string' && (data.content.includes('http') || data.content.startsWith('{'))) {\n try {\n const parsed = JSON.parse(data.content);\n mediaUrl = parsed.href || parsed.url || parsed.thumb || parsed.normalUrl;\n } catch {\n if (data.content.startsWith('http')) mediaUrl = data.content;\n }\n }\n }\n if (!mediaUrl && data.url && (data.url.includes('.jpg') || data.url.includes('.png'))) {\n mediaUrl = data.url;\n }\n\n if (!mediaUrl && data.quote) {\n try {\n const q = data.quote;\n if (q.attach) {\n const attach = typeof q.attach === 'string' ? JSON.parse(q.attach) : q.attach;\n mediaUrl = attach.href || attach.url || attach.thumb || attach.normalUrl;\n }\n if (!mediaUrl && (q.href || q.url)) {\n mediaUrl = q.href || q.url;\n }\n } catch {} \n }\n\n if (isGroup) {\n const mentions = data.mentions || data.mentionIds || [];\n const content = typeof data.content === 'string' ? data.content : (data.msg || '');\n \n // STRICT: Check UID Only\n const isMentioned = Array.isArray(mentions)\n ? mentions.some((m: any) => String(m.uid || m.id || m) === ownId)\n : false;\n \n // DEBUG Group logic\n console.log(`[ZaloX] Group Check (Strict): ownId=${ownId} mentions=${JSON.stringify(mentions)} isMentioned=${isMentioned}`);\n\n // Allow /whoami bypass\n if (content.trim() === '/whoami') {\n // Pass\n } else if (!isMentioned && !content.startsWith('/')) {\n return;\n }\n }\n\n statusSink?.({ lastInboundAt: Date.now() });\n\n const normalized = {\n threadId,\n msgId: String(data.msgId || data.cliMsgId || ''),\n content: (() => {\n let c = data.content || data.msg || '';\n if (typeof c !== 'string') return '';\n // Cleanup logic removed as per request (no string replacement)\n return c;\n })(),\n msgType,\n mediaUrl,\n timestamp: data.ts ? Math.floor(data.ts / 1000) : Math.floor(Date.now() / 1000),\n isGroup,\n senderId,\n senderName: data.dName || data.senderName || undefined,\n groupName: data.threadName || data.groupName || undefined,\n };\n\n if (!normalized.content?.trim() && !normalized.mediaUrl) return;\n\n try {\n const reactThreadType = isGroup ? ThreadType.Group : ThreadType.User;\n api.addReaction(Reactions.HEART, {\n threadId: normalized.threadId,\n type: reactThreadType,\n data: {\n msgId: String(data.msgId || ''),\n cliMsgId: String(data.cliMsgId || ''),\n },\n }).catch(() => {});\n } catch {}\n\n processMessage(normalized, account, config, runtime, profile, statusSink).catch((err) => {\n runtime.error?.(`[${account.accountId}] ZaloX process error: ${String(err)}`);\n });\n });\n\n listener.on('error', (err: any) => {\n runtime.error?.(`[${account.accountId}] ZaloX listener error: ${err.message || err}`);\n });\n\n listener.start({ retryOnClose: true });\n runtime.log?.(`[${account.accountId}] ZaloX: WebSocket listener started`);\n\n const stop = () => {\n stopped = true;\n try {\n listener.stop();\n } catch {}\n };\n\n abortSignal.addEventListener('abort', stop, { once: true });\n\n return { stop };\n}\n
|