Files
openclaw/extensions/whatsapp/src/accounts.ts
2026-03-27 23:26:26 +00:00

177 lines
5.5 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import {
createAccountListHelpers,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
resolveAccountEntry,
resolveMergedAccountConfig,
resolveUserPath,
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-core";
import { resolveOAuthDir } from "openclaw/plugin-sdk/state-paths";
import { hasWebCredsSync } from "./creds-files.js";
import type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "./runtime-api.js";
export type ResolvedWhatsAppAccount = {
accountId: string;
name?: string;
enabled: boolean;
sendReadReceipts: boolean;
messagePrefix?: string;
defaultTo?: string;
authDir: string;
isLegacyAuthDir: boolean;
selfChatMode?: boolean;
allowFrom?: string[];
groupAllowFrom?: string[];
groupPolicy?: GroupPolicy;
dmPolicy?: DmPolicy;
textChunkLimit?: number;
chunkMode?: "length" | "newline";
mediaMaxMb?: number;
blockStreaming?: boolean;
ackReaction?: WhatsAppAccountConfig["ackReaction"];
groups?: WhatsAppAccountConfig["groups"];
debounceMs?: number;
};
export const DEFAULT_WHATSAPP_MEDIA_MAX_MB = 50;
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } =
createAccountListHelpers("whatsapp");
export const listWhatsAppAccountIds = listAccountIds;
export const resolveDefaultWhatsAppAccountId = resolveDefaultAccountId;
export function listWhatsAppAuthDirs(cfg: OpenClawConfig): string[] {
const oauthDir = resolveOAuthDir();
const whatsappDir = path.join(oauthDir, "whatsapp");
const authDirs = new Set<string>([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]);
const accountIds = listConfiguredAccountIds(cfg);
for (const accountId of accountIds) {
authDirs.add(resolveWhatsAppAuthDir({ cfg, accountId }).authDir);
}
try {
const entries = fs.readdirSync(whatsappDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
authDirs.add(path.join(whatsappDir, entry.name));
}
} catch {
// ignore missing dirs
}
return Array.from(authDirs);
}
export function hasAnyWhatsAppAuth(cfg: OpenClawConfig): boolean {
return listWhatsAppAuthDirs(cfg).some((authDir) => hasWebCredsSync(authDir));
}
function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): WhatsAppAccountConfig | undefined {
return resolveAccountEntry(cfg.channels?.whatsapp?.accounts, accountId);
}
function resolveDefaultAuthDir(accountId: string): string {
return path.join(resolveOAuthDir(), "whatsapp", normalizeAccountId(accountId));
}
function resolveLegacyAuthDir(): string {
// Legacy Baileys creds lived in the same directory as OAuth tokens.
return resolveOAuthDir();
}
function legacyAuthExists(authDir: string): boolean {
try {
return fs.existsSync(path.join(authDir, "creds.json"));
} catch {
return false;
}
}
export function resolveWhatsAppAuthDir(params: { cfg: OpenClawConfig; accountId: string }): {
authDir: string;
isLegacy: boolean;
} {
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
const account = resolveAccountConfig(params.cfg, accountId);
const configured = account?.authDir?.trim();
if (configured) {
return { authDir: resolveUserPath(configured), isLegacy: false };
}
const defaultDir = resolveDefaultAuthDir(accountId);
if (accountId === DEFAULT_ACCOUNT_ID) {
const legacyDir = resolveLegacyAuthDir();
if (legacyAuthExists(legacyDir) && !legacyAuthExists(defaultDir)) {
return { authDir: legacyDir, isLegacy: true };
}
}
return { authDir: defaultDir, isLegacy: false };
}
export function resolveWhatsAppAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): ResolvedWhatsAppAccount {
const rootCfg = params.cfg.channels?.whatsapp;
const accountId = params.accountId?.trim() || resolveDefaultWhatsAppAccountId(params.cfg);
const merged = resolveMergedAccountConfig<WhatsAppAccountConfig>({
channelConfig: rootCfg as WhatsAppAccountConfig | undefined,
accounts: rootCfg?.accounts as Record<string, Partial<WhatsAppAccountConfig>> | undefined,
accountId,
omitKeys: ["defaultAccount"],
});
const enabled = merged.enabled !== false;
const { authDir, isLegacy } = resolveWhatsAppAuthDir({
cfg: params.cfg,
accountId,
});
return {
accountId,
name: merged.name?.trim() || undefined,
enabled,
sendReadReceipts: merged.sendReadReceipts ?? true,
messagePrefix: merged.messagePrefix ?? params.cfg.messages?.messagePrefix,
defaultTo: merged.defaultTo,
authDir,
isLegacyAuthDir: isLegacy,
selfChatMode: merged.selfChatMode,
dmPolicy: merged.dmPolicy,
allowFrom: merged.allowFrom,
groupAllowFrom: merged.groupAllowFrom,
groupPolicy: merged.groupPolicy,
textChunkLimit: merged.textChunkLimit,
chunkMode: merged.chunkMode,
mediaMaxMb: merged.mediaMaxMb,
blockStreaming: merged.blockStreaming,
ackReaction: merged.ackReaction,
groups: merged.groups,
debounceMs: merged.debounceMs,
};
}
export function resolveWhatsAppMediaMaxBytes(
account: Pick<ResolvedWhatsAppAccount, "mediaMaxMb">,
): number {
const mediaMaxMb =
typeof account.mediaMaxMb === "number" && account.mediaMaxMb > 0
? account.mediaMaxMb
: DEFAULT_WHATSAPP_MEDIA_MAX_MB;
return mediaMaxMb * 1024 * 1024;
}
export function listEnabledWhatsAppAccounts(cfg: OpenClawConfig): ResolvedWhatsAppAccount[] {
return listWhatsAppAccountIds(cfg)
.map((accountId) => resolveWhatsAppAccount({ cfg, accountId }))
.filter((account) => account.enabled);
}