@gakr-gakr/whatsapp 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.
- package/action-runtime-api.ts +1 -0
- package/action-runtime.runtime.ts +1 -0
- package/api.ts +67 -0
- package/auth-presence.ts +80 -0
- package/autobot.plugin.json +23 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/constants.ts +1 -0
- package/contract-api.ts +29 -0
- package/directory-contract-api.ts +4 -0
- package/doctor-contract-api.ts +8 -0
- package/index.ts +16 -0
- package/legacy-session-surface-api.ts +6 -0
- package/legacy-state-migrations-api.ts +1 -0
- package/light-runtime-api.ts +12 -0
- package/login-qr-api.ts +1 -0
- package/login-qr-runtime.ts +23 -0
- package/outbound-payload-test-api.ts +1 -0
- package/package.json +76 -0
- package/runtime-api.ts +84 -0
- package/secret-contract-api.ts +4 -0
- package/security-contract-api.ts +4 -0
- package/setup-entry.ts +21 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-config.ts +77 -0
- package/src/account-ids.ts +17 -0
- package/src/account-types.ts +5 -0
- package/src/accounts.ts +176 -0
- package/src/action-runtime-target-auth.ts +27 -0
- package/src/action-runtime.ts +76 -0
- package/src/active-listener.ts +17 -0
- package/src/agent-tools-login.ts +113 -0
- package/src/approval-auth.ts +27 -0
- package/src/auth-store.runtime.ts +1 -0
- package/src/auth-store.ts +494 -0
- package/src/auto-reply/config.runtime.ts +16 -0
- package/src/auto-reply/constants.ts +1 -0
- package/src/auto-reply/deliver-reply.ts +332 -0
- package/src/auto-reply/loggers.ts +6 -0
- package/src/auto-reply/mentions.ts +131 -0
- package/src/auto-reply/monitor/ack-reaction.ts +99 -0
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
- package/src/auto-reply/monitor/broadcast.ts +153 -0
- package/src/auto-reply/monitor/commands.ts +19 -0
- package/src/auto-reply/monitor/echo.ts +64 -0
- package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
- package/src/auto-reply/monitor/group-activation.ts +73 -0
- package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
- package/src/auto-reply/monitor/group-gating.ts +218 -0
- package/src/auto-reply/monitor/group-members.ts +65 -0
- package/src/auto-reply/monitor/inbound-context.ts +92 -0
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
- package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
- package/src/auto-reply/monitor/last-route.ts +61 -0
- package/src/auto-reply/monitor/listener-log.ts +28 -0
- package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
- package/src/auto-reply/monitor/message-line.ts +54 -0
- package/src/auto-reply/monitor/on-message.ts +333 -0
- package/src/auto-reply/monitor/peer.ts +17 -0
- package/src/auto-reply/monitor/process-message.ts +584 -0
- package/src/auto-reply/monitor/runtime-api.ts +36 -0
- package/src/auto-reply/monitor/status-reaction.ts +108 -0
- package/src/auto-reply/monitor-state.ts +114 -0
- package/src/auto-reply/monitor.ts +720 -0
- package/src/auto-reply/reply-resolver.runtime.ts +1 -0
- package/src/auto-reply/types.ts +48 -0
- package/src/auto-reply/util.ts +62 -0
- package/src/auto-reply.impl.ts +6 -0
- package/src/auto-reply.ts +1 -0
- package/src/channel-actions.runtime.ts +7 -0
- package/src/channel-actions.ts +85 -0
- package/src/channel-outbound.ts +87 -0
- package/src/channel-react-action.runtime.ts +10 -0
- package/src/channel-react-action.ts +247 -0
- package/src/channel.runtime.ts +117 -0
- package/src/channel.setup.ts +32 -0
- package/src/channel.ts +356 -0
- package/src/command-policy.ts +7 -0
- package/src/config-accessors.ts +22 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +24 -0
- package/src/connection-controller-registry.ts +49 -0
- package/src/connection-controller.ts +680 -0
- package/src/creds-files.ts +19 -0
- package/src/creds-persistence.ts +71 -0
- package/src/directory-config.ts +40 -0
- package/src/doctor-contract.ts +11 -0
- package/src/doctor.ts +56 -0
- package/src/document-filename.ts +17 -0
- package/src/group-intro.ts +15 -0
- package/src/group-policy.ts +40 -0
- package/src/group-session-contract.ts +20 -0
- package/src/group-session-key.ts +42 -0
- package/src/heartbeat.ts +34 -0
- package/src/identity.ts +164 -0
- package/src/inbound/access-control.ts +187 -0
- package/src/inbound/dedupe.ts +132 -0
- package/src/inbound/extract.ts +484 -0
- package/src/inbound/lifecycle.ts +39 -0
- package/src/inbound/media.ts +128 -0
- package/src/inbound/monitor.ts +1042 -0
- package/src/inbound/outbound-mentions.ts +260 -0
- package/src/inbound/runtime-api.ts +7 -0
- package/src/inbound/save-media.runtime.ts +1 -0
- package/src/inbound/send-api.ts +203 -0
- package/src/inbound/send-result.ts +109 -0
- package/src/inbound/types.ts +107 -0
- package/src/inbound-policy.ts +215 -0
- package/src/inbound.ts +9 -0
- package/src/login-qr.ts +542 -0
- package/src/login.ts +83 -0
- package/src/media.ts +10 -0
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
- package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
- package/src/normalize-target.ts +148 -0
- package/src/normalize.ts +8 -0
- package/src/outbound-adapter.ts +36 -0
- package/src/outbound-base.ts +256 -0
- package/src/outbound-media-contract.ts +307 -0
- package/src/outbound-media.runtime.ts +41 -0
- package/src/outbound-send-deps.ts +1 -0
- package/src/outbound-test-support.ts +16 -0
- package/src/qa-driver.runtime.ts +189 -0
- package/src/qr-image.ts +1 -0
- package/src/qr-terminal.ts +1 -0
- package/src/quoted-message.ts +184 -0
- package/src/reaction-level.ts +24 -0
- package/src/reconnect.ts +55 -0
- package/src/resolve-outbound-target.ts +58 -0
- package/src/runtime-api.ts +59 -0
- package/src/runtime-group-policy.ts +16 -0
- package/src/runtime.ts +9 -0
- package/src/security-contract.ts +47 -0
- package/src/security-fix.ts +71 -0
- package/src/send.ts +342 -0
- package/src/session-contract.ts +43 -0
- package/src/session-errors.ts +125 -0
- package/src/session-route.ts +32 -0
- package/src/session.runtime.ts +8 -0
- package/src/session.ts +327 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-finalize.ts +450 -0
- package/src/setup-surface.ts +71 -0
- package/src/setup-test-helpers.ts +217 -0
- package/src/shared.ts +291 -0
- package/src/socket-timing.ts +38 -0
- package/src/state-migrations.ts +55 -0
- package/src/status-issues.ts +185 -0
- package/src/system-prompt.ts +31 -0
- package/src/targets-runtime.ts +221 -0
- package/src/text-runtime.ts +18 -0
- package/src/vcard.ts +84 -0
- package/targets.ts +5 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getReplyFromConfig } from "autobot/plugin-sdk/reply-runtime";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { WebInboundMessage } from "../inbound/types.js";
|
|
2
|
+
import type { ReconnectPolicy } from "../reconnect.js";
|
|
3
|
+
import type { WhatsAppSocketTimingOptions } from "../socket-timing.js";
|
|
4
|
+
|
|
5
|
+
export type WebChannelHealthState =
|
|
6
|
+
| "starting"
|
|
7
|
+
| "healthy"
|
|
8
|
+
| "stale"
|
|
9
|
+
| "reconnecting"
|
|
10
|
+
| "conflict"
|
|
11
|
+
| "logged-out"
|
|
12
|
+
| "stopped";
|
|
13
|
+
|
|
14
|
+
export type WebInboundMsg = WebInboundMessage;
|
|
15
|
+
|
|
16
|
+
export type WebChannelStatus = {
|
|
17
|
+
running: boolean;
|
|
18
|
+
connected: boolean;
|
|
19
|
+
reconnectAttempts: number;
|
|
20
|
+
lastConnectedAt?: number | null;
|
|
21
|
+
lastDisconnect?: {
|
|
22
|
+
at: number;
|
|
23
|
+
status?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
loggedOut?: boolean;
|
|
26
|
+
} | null;
|
|
27
|
+
lastInboundAt?: number | null;
|
|
28
|
+
lastMessageAt?: number | null;
|
|
29
|
+
lastEventAt?: number | null;
|
|
30
|
+
lastTransportActivityAt?: number | null;
|
|
31
|
+
lastError?: string | null;
|
|
32
|
+
healthState?: WebChannelHealthState;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type WebMonitorTuning = {
|
|
36
|
+
reconnect?: Partial<ReconnectPolicy>;
|
|
37
|
+
socketTiming?: WhatsAppSocketTimingOptions;
|
|
38
|
+
heartbeatSeconds?: number;
|
|
39
|
+
transportTimeoutMs?: number;
|
|
40
|
+
messageTimeoutMs?: number;
|
|
41
|
+
watchdogCheckMs?: number;
|
|
42
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
43
|
+
statusSink?: (status: WebChannelStatus) => void;
|
|
44
|
+
/** WhatsApp account id. Default: "default". */
|
|
45
|
+
accountId?: string;
|
|
46
|
+
/** Debounce window (ms) for batching rapid consecutive messages from the same sender. */
|
|
47
|
+
debounceMs?: number;
|
|
48
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "autobot/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
|
|
3
|
+
export function elide(text?: string, limit = 400) {
|
|
4
|
+
if (!text) {
|
|
5
|
+
return text;
|
|
6
|
+
}
|
|
7
|
+
if (text.length <= limit) {
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isLikelyWhatsAppCryptoError(reason: unknown) {
|
|
14
|
+
const formatReason = (value: unknown): string => {
|
|
15
|
+
if (value == null) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (value instanceof Error) {
|
|
22
|
+
return `${value.message}\n${value.stack ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === "object") {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return Object.prototype.toString.call(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "boolean") {
|
|
35
|
+
return String(value);
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "bigint") {
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "symbol") {
|
|
41
|
+
return value.description ?? value.toString();
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "function") {
|
|
44
|
+
return value.name ? `[function ${value.name}]` : "[function]";
|
|
45
|
+
}
|
|
46
|
+
return Object.prototype.toString.call(value);
|
|
47
|
+
};
|
|
48
|
+
const raw =
|
|
49
|
+
reason instanceof Error ? `${reason.message}\n${reason.stack ?? ""}` : formatReason(reason);
|
|
50
|
+
const haystack = normalizeLowercaseStringOrEmpty(raw);
|
|
51
|
+
const hasAuthError =
|
|
52
|
+
haystack.includes("unsupported state or unable to authenticate data") ||
|
|
53
|
+
haystack.includes("bad mac");
|
|
54
|
+
if (!hasAuthError) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
haystack.includes("baileys") ||
|
|
59
|
+
haystack.includes("noise-handler") ||
|
|
60
|
+
haystack.includes("aesdecryptgcm")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { HEARTBEAT_PROMPT, stripHeartbeatToken } from "autobot/plugin-sdk/reply-runtime";
|
|
2
|
+
export { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "autobot/plugin-sdk/reply-runtime";
|
|
3
|
+
|
|
4
|
+
export { DEFAULT_WEB_MEDIA_BYTES } from "./auto-reply/constants.js";
|
|
5
|
+
export { monitorWebChannel } from "./auto-reply/monitor.js";
|
|
6
|
+
export type { WebChannelStatus, WebMonitorTuning } from "./auto-reply/types.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./auto-reply.impl.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createActionGate } from "autobot/plugin-sdk/channel-actions";
|
|
2
|
+
import type { ChannelMessageActionName } from "autobot/plugin-sdk/channel-contract";
|
|
3
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
4
|
+
|
|
5
|
+
export { listWhatsAppAccountIds, resolveWhatsAppAccount } from "./accounts.js";
|
|
6
|
+
export { resolveWhatsAppReactionLevel } from "./reaction-level.js";
|
|
7
|
+
export { createActionGate, type ChannelMessageActionName, type AutoBotConfig };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listWhatsAppAccountIds,
|
|
3
|
+
resolveWhatsAppAccount,
|
|
4
|
+
createActionGate,
|
|
5
|
+
type ChannelMessageActionName,
|
|
6
|
+
type AutoBotConfig,
|
|
7
|
+
resolveWhatsAppReactionLevel,
|
|
8
|
+
} from "./channel-actions.runtime.js";
|
|
9
|
+
|
|
10
|
+
function areWhatsAppAgentReactionsEnabled(params: { cfg: AutoBotConfig; accountId?: string }) {
|
|
11
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
15
|
+
if (!gate("reactions")) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return resolveWhatsAppReactionLevel({
|
|
19
|
+
cfg: params.cfg,
|
|
20
|
+
accountId: params.accountId,
|
|
21
|
+
}).agentReactionsEnabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasAnyWhatsAppAccountWithAgentReactionsEnabled(cfg: AutoBotConfig) {
|
|
25
|
+
if (!cfg.channels?.whatsapp) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return listWhatsAppAccountIds(cfg).some((accountId) => {
|
|
29
|
+
const account = resolveWhatsAppAccount({ cfg, accountId });
|
|
30
|
+
if (!account.enabled) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return areWhatsAppAgentReactionsEnabled({
|
|
34
|
+
cfg,
|
|
35
|
+
accountId,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveWhatsAppAgentReactionGuidance(params: {
|
|
41
|
+
cfg: AutoBotConfig;
|
|
42
|
+
accountId?: string;
|
|
43
|
+
}) {
|
|
44
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
48
|
+
if (!gate("reactions")) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const resolved = resolveWhatsAppReactionLevel({
|
|
52
|
+
cfg: params.cfg,
|
|
53
|
+
accountId: params.accountId,
|
|
54
|
+
});
|
|
55
|
+
if (!resolved.agentReactionsEnabled) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return resolved.agentReactionGuidance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function describeWhatsAppMessageActions(params: {
|
|
62
|
+
cfg: AutoBotConfig;
|
|
63
|
+
accountId?: string | null;
|
|
64
|
+
}): { actions: ChannelMessageActionName[] } | null {
|
|
65
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
69
|
+
const actions = new Set<ChannelMessageActionName>();
|
|
70
|
+
const canReact =
|
|
71
|
+
params.accountId != null
|
|
72
|
+
? areWhatsAppAgentReactionsEnabled({
|
|
73
|
+
cfg: params.cfg,
|
|
74
|
+
accountId: params.accountId ?? undefined,
|
|
75
|
+
})
|
|
76
|
+
: hasAnyWhatsAppAccountWithAgentReactionsEnabled(params.cfg);
|
|
77
|
+
if (canReact) {
|
|
78
|
+
actions.add("react");
|
|
79
|
+
}
|
|
80
|
+
if (gate("polls")) {
|
|
81
|
+
actions.add("poll");
|
|
82
|
+
}
|
|
83
|
+
actions.add("upload-file");
|
|
84
|
+
return { actions: Array.from(actions) };
|
|
85
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMessageReceiptFromOutboundResults,
|
|
3
|
+
defineChannelMessageAdapter,
|
|
4
|
+
type ChannelMessageSendResult,
|
|
5
|
+
} from "autobot/plugin-sdk/channel-message";
|
|
6
|
+
import { chunkText } from "autobot/plugin-sdk/reply-chunking";
|
|
7
|
+
import { createWhatsAppOutboundBase } from "./outbound-base.js";
|
|
8
|
+
import { normalizeWhatsAppPayloadTextPreservingIndentation } from "./outbound-media-contract.js";
|
|
9
|
+
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
|
|
10
|
+
import { getWhatsAppRuntime } from "./runtime.js";
|
|
11
|
+
import { sendMessageWhatsApp, sendPollWhatsApp } from "./send.js";
|
|
12
|
+
|
|
13
|
+
export function normalizeWhatsAppChannelPayloadText(text: string | undefined): string {
|
|
14
|
+
return normalizeWhatsAppPayloadTextPreservingIndentation(text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeWhatsAppChannelSendText(text: string | undefined): string {
|
|
18
|
+
const normalized = normalizeWhatsAppChannelPayloadText(text);
|
|
19
|
+
return normalized.trim() ? normalized : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const whatsappChannelOutbound = {
|
|
23
|
+
...createWhatsAppOutboundBase({
|
|
24
|
+
chunker: chunkText,
|
|
25
|
+
sendMessageWhatsApp: async (to, text, options) =>
|
|
26
|
+
await sendMessageWhatsApp(to, text, {
|
|
27
|
+
...options,
|
|
28
|
+
preserveLeadingWhitespace: true,
|
|
29
|
+
}),
|
|
30
|
+
sendPollWhatsApp,
|
|
31
|
+
shouldLogVerbose: () => getWhatsAppRuntime().logging.shouldLogVerbose(),
|
|
32
|
+
resolveTarget: ({ to, allowFrom, mode }) =>
|
|
33
|
+
resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
|
|
34
|
+
normalizeText: normalizeWhatsAppChannelSendText,
|
|
35
|
+
}),
|
|
36
|
+
sendTextOnlyErrorPayloads: true,
|
|
37
|
+
normalizePayload: ({ payload }: { payload: { text?: string } }) => ({
|
|
38
|
+
...payload,
|
|
39
|
+
text: normalizeWhatsAppChannelPayloadText(payload.text),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function toWhatsAppMessageSendResult(
|
|
44
|
+
result: Awaited<ReturnType<NonNullable<typeof whatsappChannelOutbound.sendText>>>,
|
|
45
|
+
replyToId?: string | null,
|
|
46
|
+
): ChannelMessageSendResult {
|
|
47
|
+
const source = result as typeof result & { toJid?: string };
|
|
48
|
+
const receipt =
|
|
49
|
+
result.receipt ??
|
|
50
|
+
createMessageReceiptFromOutboundResults({
|
|
51
|
+
results: result.messageId
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
channel: "whatsapp",
|
|
55
|
+
messageId: result.messageId,
|
|
56
|
+
toJid: source.toJid,
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
: [],
|
|
60
|
+
kind: "text",
|
|
61
|
+
...(replyToId ? { replyToId } : {}),
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
messageId: result.messageId || receipt.primaryPlatformMessageId,
|
|
65
|
+
receipt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const whatsappMessageAdapter = defineChannelMessageAdapter({
|
|
70
|
+
id: "whatsapp",
|
|
71
|
+
durableFinal: {
|
|
72
|
+
capabilities: {
|
|
73
|
+
text: true,
|
|
74
|
+
replyTo: true,
|
|
75
|
+
messageSendingHooks: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
send: {
|
|
79
|
+
text: async (ctx) =>
|
|
80
|
+
toWhatsAppMessageSendResult(
|
|
81
|
+
await whatsappChannelOutbound.sendText!({
|
|
82
|
+
...ctx,
|
|
83
|
+
}),
|
|
84
|
+
ctx.replyToId,
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readStringOrNumberParam, readStringParam } from "autobot/plugin-sdk/channel-actions";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
|
|
4
|
+
export { resolveReactionMessageId } from "autobot/plugin-sdk/channel-actions";
|
|
5
|
+
export { handleWhatsAppAction } from "./action-runtime.js";
|
|
6
|
+
export { resolveAuthorizedWhatsAppOutboundTarget } from "./action-runtime-target-auth.js";
|
|
7
|
+
export { resolveWhatsAppAccount, resolveWhatsAppMediaMaxBytes } from "./accounts.js";
|
|
8
|
+
export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
|
9
|
+
export { sendMessageWhatsApp } from "./send.js";
|
|
10
|
+
export { readStringOrNumberParam, readStringParam, type AutoBotConfig };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { jsonResult } from "autobot/plugin-sdk/channel-actions";
|
|
2
|
+
import {
|
|
3
|
+
isWhatsAppGroupJid,
|
|
4
|
+
resolveAuthorizedWhatsAppOutboundTarget,
|
|
5
|
+
resolveWhatsAppAccount,
|
|
6
|
+
resolveWhatsAppMediaMaxBytes,
|
|
7
|
+
resolveReactionMessageId,
|
|
8
|
+
handleWhatsAppAction,
|
|
9
|
+
normalizeWhatsAppTarget,
|
|
10
|
+
readStringOrNumberParam,
|
|
11
|
+
readStringParam,
|
|
12
|
+
sendMessageWhatsApp,
|
|
13
|
+
type AutoBotConfig,
|
|
14
|
+
} from "./channel-react-action.runtime.js";
|
|
15
|
+
|
|
16
|
+
const WHATSAPP_CHANNEL = "whatsapp" as const;
|
|
17
|
+
|
|
18
|
+
type WhatsAppMessageActionParams = {
|
|
19
|
+
action: string;
|
|
20
|
+
params: Record<string, unknown>;
|
|
21
|
+
cfg: AutoBotConfig;
|
|
22
|
+
accountId?: string | null;
|
|
23
|
+
requesterSenderId?: string | null;
|
|
24
|
+
mediaAccess?: {
|
|
25
|
+
localRoots?: readonly string[];
|
|
26
|
+
readFile?: (filePath: string) => Promise<Buffer>;
|
|
27
|
+
};
|
|
28
|
+
mediaLocalRoots?: readonly string[];
|
|
29
|
+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
30
|
+
toolContext?: {
|
|
31
|
+
currentChannelId?: string | null;
|
|
32
|
+
currentChannelProvider?: string | null;
|
|
33
|
+
currentMessageId?: string | number | null;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function readUploadFileMediaSource(args: Record<string, unknown>): string | undefined {
|
|
38
|
+
return (
|
|
39
|
+
readStringParam(args, "media", { trim: false }) ??
|
|
40
|
+
readStringParam(args, "mediaUrl", { trim: false }) ??
|
|
41
|
+
readStringParam(args, "filePath", { trim: false }) ??
|
|
42
|
+
readStringParam(args, "path", { trim: false }) ??
|
|
43
|
+
readStringParam(args, "fileUrl", { trim: false })
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readUploadFileCaptionText(args: Record<string, unknown>): string {
|
|
48
|
+
return (
|
|
49
|
+
readStringParam(args, "message", { allowEmpty: true }) ??
|
|
50
|
+
readStringParam(args, "content", { allowEmpty: true }) ??
|
|
51
|
+
readStringParam(args, "caption", { allowEmpty: true }) ??
|
|
52
|
+
""
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readBooleanParam(args: Record<string, unknown>, key: string): boolean | undefined {
|
|
57
|
+
const value = args[key];
|
|
58
|
+
if (typeof value === "boolean") {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "string") {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const normalized = value.trim().toLowerCase();
|
|
65
|
+
if (normalized === "true") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (normalized === "false") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasUploadFileBufferPayload(args: Record<string, unknown>): boolean {
|
|
75
|
+
return readStringParam(args, "buffer", { trim: false }) !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractBase64Payload(encoded: string): string {
|
|
79
|
+
const match = /^data:[^;]+;base64,(.*)$/i.exec(encoded.trim());
|
|
80
|
+
return match ? match[1] : encoded;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function estimateBase64DecodedBytes(encoded: string): number {
|
|
84
|
+
const compact = extractBase64Payload(encoded).replace(/\s/g, "");
|
|
85
|
+
if (!compact) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
|
|
89
|
+
return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function decodeUploadFileMediaPayload(params: {
|
|
93
|
+
args: Record<string, unknown>;
|
|
94
|
+
encoded: string;
|
|
95
|
+
maxBytes?: number;
|
|
96
|
+
}):
|
|
97
|
+
| {
|
|
98
|
+
buffer: Buffer;
|
|
99
|
+
contentType?: string;
|
|
100
|
+
fileName?: string;
|
|
101
|
+
}
|
|
102
|
+
| undefined {
|
|
103
|
+
if (params.maxBytes !== undefined) {
|
|
104
|
+
const estimatedBytes = estimateBase64DecodedBytes(params.encoded);
|
|
105
|
+
if (estimatedBytes > params.maxBytes) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`WhatsApp upload-file buffer exceeds configured media limit (${estimatedBytes} bytes > ${params.maxBytes} bytes).`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const contentType =
|
|
112
|
+
readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
|
|
113
|
+
const fileName =
|
|
114
|
+
readStringParam(params.args, "filename") ?? readStringParam(params.args, "fileName");
|
|
115
|
+
const buffer = Buffer.from(extractBase64Payload(params.encoded), "base64");
|
|
116
|
+
if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`WhatsApp upload-file buffer exceeds configured media limit (${buffer.byteLength} bytes > ${params.maxBytes} bytes).`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
buffer,
|
|
123
|
+
...(contentType ? { contentType } : {}),
|
|
124
|
+
...(fileName ? { fileName } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function handleWhatsAppUploadFileAction(params: WhatsAppMessageActionParams) {
|
|
129
|
+
const mediaUrl = readUploadFileMediaSource(params.params);
|
|
130
|
+
const encodedPayload = readStringParam(params.params, "buffer", { trim: false });
|
|
131
|
+
if (!mediaUrl && !hasUploadFileBufferPayload(params.params)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"WhatsApp upload-file requires media, mediaUrl, filePath, path, fileUrl, or buffer.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const to = readStringParam(params.params, "to", { required: true });
|
|
137
|
+
const resolved = resolveAuthorizedWhatsAppOutboundTarget({
|
|
138
|
+
cfg: params.cfg,
|
|
139
|
+
chatJid: to,
|
|
140
|
+
accountId: params.accountId ?? undefined,
|
|
141
|
+
actionLabel: "upload-file",
|
|
142
|
+
});
|
|
143
|
+
const account = resolveWhatsAppAccount({
|
|
144
|
+
cfg: params.cfg,
|
|
145
|
+
accountId: resolved.accountId,
|
|
146
|
+
});
|
|
147
|
+
const mediaPayload = encodedPayload
|
|
148
|
+
? decodeUploadFileMediaPayload({
|
|
149
|
+
args: params.params,
|
|
150
|
+
encoded: encodedPayload,
|
|
151
|
+
maxBytes: resolveWhatsAppMediaMaxBytes(account),
|
|
152
|
+
})
|
|
153
|
+
: undefined;
|
|
154
|
+
const result = await sendMessageWhatsApp(resolved.to, readUploadFileCaptionText(params.params), {
|
|
155
|
+
verbose: false,
|
|
156
|
+
cfg: params.cfg,
|
|
157
|
+
...(mediaUrl && !mediaPayload ? { mediaUrl } : {}),
|
|
158
|
+
...(mediaPayload ? { mediaPayload } : {}),
|
|
159
|
+
mediaAccess: params.mediaAccess,
|
|
160
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
161
|
+
mediaReadFile: params.mediaReadFile,
|
|
162
|
+
gifPlayback: readBooleanParam(params.params, "gifPlayback") ?? undefined,
|
|
163
|
+
audioAsVoice:
|
|
164
|
+
readBooleanParam(params.params, "asVoice") ??
|
|
165
|
+
readBooleanParam(params.params, "audioAsVoice") ??
|
|
166
|
+
undefined,
|
|
167
|
+
forceDocument:
|
|
168
|
+
readBooleanParam(params.params, "forceDocument") ??
|
|
169
|
+
readBooleanParam(params.params, "asDocument") ??
|
|
170
|
+
undefined,
|
|
171
|
+
accountId: resolved.accountId,
|
|
172
|
+
});
|
|
173
|
+
return jsonResult({
|
|
174
|
+
ok: true,
|
|
175
|
+
channel: WHATSAPP_CHANNEL,
|
|
176
|
+
action: "upload-file",
|
|
177
|
+
messageId: result.messageId,
|
|
178
|
+
toJid: result.toJid,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function handleWhatsAppMessageAction(params: WhatsAppMessageActionParams) {
|
|
183
|
+
if (params.action === "upload-file") {
|
|
184
|
+
return await handleWhatsAppUploadFileAction(params);
|
|
185
|
+
}
|
|
186
|
+
if (params.action !== "react") {
|
|
187
|
+
throw new Error(`Action ${params.action} is not supported for provider ${WHATSAPP_CHANNEL}.`);
|
|
188
|
+
}
|
|
189
|
+
const isWhatsAppSource = params.toolContext?.currentChannelProvider === WHATSAPP_CHANNEL;
|
|
190
|
+
const explicitTarget =
|
|
191
|
+
readStringParam(params.params, "chatJid") ?? readStringParam(params.params, "to");
|
|
192
|
+
const normalizedTarget = explicitTarget ? normalizeWhatsAppTarget(explicitTarget) : null;
|
|
193
|
+
const normalizedCurrent =
|
|
194
|
+
isWhatsAppSource && params.toolContext?.currentChannelId
|
|
195
|
+
? normalizeWhatsAppTarget(params.toolContext.currentChannelId)
|
|
196
|
+
: null;
|
|
197
|
+
const isCrossChat =
|
|
198
|
+
normalizedTarget != null &&
|
|
199
|
+
(normalizedCurrent == null || normalizedTarget !== normalizedCurrent);
|
|
200
|
+
const scopedContext =
|
|
201
|
+
!isWhatsAppSource || isCrossChat || !params.toolContext
|
|
202
|
+
? undefined
|
|
203
|
+
: {
|
|
204
|
+
currentChannelId: params.toolContext.currentChannelId ?? undefined,
|
|
205
|
+
currentChannelProvider: params.toolContext.currentChannelProvider ?? undefined,
|
|
206
|
+
currentMessageId: params.toolContext.currentMessageId ?? undefined,
|
|
207
|
+
};
|
|
208
|
+
const messageIdRaw = resolveReactionMessageId({
|
|
209
|
+
args: params.params,
|
|
210
|
+
toolContext: scopedContext,
|
|
211
|
+
});
|
|
212
|
+
if (messageIdRaw == null) {
|
|
213
|
+
readStringParam(params.params, "messageId", { required: true });
|
|
214
|
+
}
|
|
215
|
+
const messageId = String(messageIdRaw);
|
|
216
|
+
const explicitMessageId = readStringOrNumberParam(params.params, "messageId");
|
|
217
|
+
const emoji = readStringParam(params.params, "emoji", { allowEmpty: true });
|
|
218
|
+
const remove = typeof params.params.remove === "boolean" ? params.params.remove : undefined;
|
|
219
|
+
const explicitParticipant = readStringParam(params.params, "participant");
|
|
220
|
+
const inferredParticipant =
|
|
221
|
+
explicitParticipant ||
|
|
222
|
+
explicitMessageId != null ||
|
|
223
|
+
!isWhatsAppSource ||
|
|
224
|
+
isCrossChat ||
|
|
225
|
+
!isWhatsAppGroupJid(explicitTarget ?? params.toolContext?.currentChannelId ?? "")
|
|
226
|
+
? undefined
|
|
227
|
+
: typeof params.requesterSenderId === "string" && params.requesterSenderId.trim().length > 0
|
|
228
|
+
? params.requesterSenderId.trim()
|
|
229
|
+
: undefined;
|
|
230
|
+
return await handleWhatsAppAction(
|
|
231
|
+
{
|
|
232
|
+
action: "react",
|
|
233
|
+
chatJid:
|
|
234
|
+
readStringParam(params.params, "chatJid") ??
|
|
235
|
+
readStringParam(params.params, "to", { required: true }),
|
|
236
|
+
messageId,
|
|
237
|
+
emoji,
|
|
238
|
+
remove,
|
|
239
|
+
participant: explicitParticipant ?? inferredParticipant,
|
|
240
|
+
accountId: params.accountId ?? undefined,
|
|
241
|
+
fromMe: typeof params.params.fromMe === "boolean" ? params.params.fromMe : undefined,
|
|
242
|
+
},
|
|
243
|
+
params.cfg,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const handleWhatsAppReactAction = handleWhatsAppMessageAction;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startWebLoginWithQr as startWebLoginWithQrImpl,
|
|
3
|
+
waitForWebLogin as waitForWebLoginImpl,
|
|
4
|
+
} from "../login-qr-runtime.js";
|
|
5
|
+
import { getActiveWebListener as getActiveWebListenerImpl } from "./active-listener.js";
|
|
6
|
+
import {
|
|
7
|
+
getWebAuthAgeMs as getWebAuthAgeMsImpl,
|
|
8
|
+
logWebSelfId as logWebSelfIdImpl,
|
|
9
|
+
logoutWeb as logoutWebImpl,
|
|
10
|
+
readWebAuthSnapshot as readWebAuthSnapshotImpl,
|
|
11
|
+
readWebAuthState as readWebAuthStateImpl,
|
|
12
|
+
readWebAuthExistsBestEffort as readWebAuthExistsBestEffortImpl,
|
|
13
|
+
readWebAuthExistsForDecision as readWebAuthExistsForDecisionImpl,
|
|
14
|
+
readWebAuthSnapshotBestEffort as readWebAuthSnapshotBestEffortImpl,
|
|
15
|
+
readWebSelfId as readWebSelfIdImpl,
|
|
16
|
+
webAuthExists as webAuthExistsImpl,
|
|
17
|
+
} from "./auth-store.js";
|
|
18
|
+
import { monitorWebChannel as monitorWebChannelImpl } from "./auto-reply/monitor.js";
|
|
19
|
+
import { loginWeb as loginWebImpl } from "./login.js";
|
|
20
|
+
import { whatsappSetupWizard as whatsappSetupWizardImpl } from "./setup-surface.js";
|
|
21
|
+
|
|
22
|
+
type GetActiveWebListener = typeof import("./active-listener.js").getActiveWebListener;
|
|
23
|
+
type GetWebAuthAgeMs = typeof import("./auth-store.js").getWebAuthAgeMs;
|
|
24
|
+
type LogWebSelfId = typeof import("./auth-store.js").logWebSelfId;
|
|
25
|
+
type LogoutWeb = typeof import("./auth-store.js").logoutWeb;
|
|
26
|
+
type ReadWebAuthSnapshot = typeof import("./auth-store.js").readWebAuthSnapshot;
|
|
27
|
+
type ReadWebAuthState = typeof import("./auth-store.js").readWebAuthState;
|
|
28
|
+
type ReadWebAuthExistsBestEffort = typeof import("./auth-store.js").readWebAuthExistsBestEffort;
|
|
29
|
+
type ReadWebAuthExistsForDecision = typeof import("./auth-store.js").readWebAuthExistsForDecision;
|
|
30
|
+
type ReadWebAuthSnapshotBestEffort = typeof import("./auth-store.js").readWebAuthSnapshotBestEffort;
|
|
31
|
+
type ReadWebSelfId = typeof import("./auth-store.js").readWebSelfId;
|
|
32
|
+
type WebAuthExists = typeof import("./auth-store.js").webAuthExists;
|
|
33
|
+
type LoginWeb = typeof import("./login.js").loginWeb;
|
|
34
|
+
type StartWebLoginWithQr = typeof import("../login-qr-runtime.js").startWebLoginWithQr;
|
|
35
|
+
type WaitForWebLogin = typeof import("../login-qr-runtime.js").waitForWebLogin;
|
|
36
|
+
type WhatsAppSetupWizard = typeof import("./setup-surface.js").whatsappSetupWizard;
|
|
37
|
+
type MonitorWebChannel = typeof import("./auto-reply/monitor.js").monitorWebChannel;
|
|
38
|
+
|
|
39
|
+
export function getActiveWebListener(
|
|
40
|
+
...args: Parameters<GetActiveWebListener>
|
|
41
|
+
): ReturnType<GetActiveWebListener> {
|
|
42
|
+
return getActiveWebListenerImpl(...args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getWebAuthAgeMs(...args: Parameters<GetWebAuthAgeMs>): ReturnType<GetWebAuthAgeMs> {
|
|
46
|
+
return getWebAuthAgeMsImpl(...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function logWebSelfId(...args: Parameters<LogWebSelfId>): ReturnType<LogWebSelfId> {
|
|
50
|
+
return logWebSelfIdImpl(...args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logoutWeb(...args: Parameters<LogoutWeb>): ReturnType<LogoutWeb> {
|
|
54
|
+
return logoutWebImpl(...args);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readWebAuthSnapshot(
|
|
58
|
+
...args: Parameters<ReadWebAuthSnapshot>
|
|
59
|
+
): ReturnType<ReadWebAuthSnapshot> {
|
|
60
|
+
return readWebAuthSnapshotImpl(...args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readWebAuthState(
|
|
64
|
+
...args: Parameters<ReadWebAuthState>
|
|
65
|
+
): ReturnType<ReadWebAuthState> {
|
|
66
|
+
return readWebAuthStateImpl(...args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readWebAuthExistsBestEffort(
|
|
70
|
+
...args: Parameters<ReadWebAuthExistsBestEffort>
|
|
71
|
+
): ReturnType<ReadWebAuthExistsBestEffort> {
|
|
72
|
+
return readWebAuthExistsBestEffortImpl(...args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readWebAuthExistsForDecision(
|
|
76
|
+
...args: Parameters<ReadWebAuthExistsForDecision>
|
|
77
|
+
): ReturnType<ReadWebAuthExistsForDecision> {
|
|
78
|
+
return readWebAuthExistsForDecisionImpl(...args);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readWebAuthSnapshotBestEffort(
|
|
82
|
+
...args: Parameters<ReadWebAuthSnapshotBestEffort>
|
|
83
|
+
): ReturnType<ReadWebAuthSnapshotBestEffort> {
|
|
84
|
+
return readWebAuthSnapshotBestEffortImpl(...args);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function readWebSelfId(...args: Parameters<ReadWebSelfId>): ReturnType<ReadWebSelfId> {
|
|
88
|
+
return readWebSelfIdImpl(...args);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function webAuthExists(...args: Parameters<WebAuthExists>): ReturnType<WebAuthExists> {
|
|
92
|
+
return webAuthExistsImpl(...args);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function loginWeb(...args: Parameters<LoginWeb>): ReturnType<LoginWeb> {
|
|
96
|
+
return loginWebImpl(...args);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function startWebLoginWithQr(
|
|
100
|
+
...args: Parameters<StartWebLoginWithQr>
|
|
101
|
+
): ReturnType<StartWebLoginWithQr> {
|
|
102
|
+
return await startWebLoginWithQrImpl(...args);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function waitForWebLogin(
|
|
106
|
+
...args: Parameters<WaitForWebLogin>
|
|
107
|
+
): ReturnType<WaitForWebLogin> {
|
|
108
|
+
return await waitForWebLoginImpl(...args);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const whatsappSetupWizard: WhatsAppSetupWizard = { ...whatsappSetupWizardImpl };
|
|
112
|
+
|
|
113
|
+
export function monitorWebChannel(
|
|
114
|
+
...args: Parameters<MonitorWebChannel>
|
|
115
|
+
): ReturnType<MonitorWebChannel> {
|
|
116
|
+
return monitorWebChannelImpl(...args);
|
|
117
|
+
}
|