import fs from "node:fs" ;
import path from "node:path" ;
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution" ;
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env" ;
import { escapeRegExp } from "openclaw/plugin-sdk/text-runtime" ;
import { CONFIG_DIR, resolveUserPath } from "openclaw/plugin-sdk/text-runtime" ;
const WHATSAPP_FENCE_PLACEHOLDER = "\x00FENCE" ;
const WHATSAPP_INLINE_CODE_PLACEHOLDER = "\x00CODE" ;
export type WebChannel = "web" ;
export function assertWebChannel(input: string): asserts input is WebChannel {
if (input !== "web" ) {
throw new Error("Web channel must be 'web'" );
}
}
export function isSelfChatMode(
selfE164: string | null | undefined,
allowFrom?: Array<string | number> | null ,
): boolean {
if (!selfE164) {
return false ;
}
if (!Array.isArray(allowFrom) || allowFrom.length === 0 ) {
return false ;
}
const normalizedSelf = normalizeE164(selfE164);
return allowFrom.some((n) => {
if (n === "*" ) {
return false ;
}
try {
return normalizeE164(String(n)) === normalizedSelf;
} catch {
return false ;
}
});
}
export function toWhatsappJid(number: string): string {
const withoutPrefix = number.replace(/^whatsapp:/i, "" ).trim();
if (withoutPrefix.includes("@" )) {
return withoutPrefix;
}
const e164 = normalizeE164(withoutPrefix);
const digits = e164.replace(/\D/g, "" );
return `${digits}@s.whatsapp.net`;
}
export type JidToE164Options = {
authDir?: string;
lidMappingDirs?: string[];
logMissing?: boolean ;
};
type LidLookup = {
getPNForLID?: (jid: string) => Promise<string | null >;
};
function resolveLidMappingDirs(params: { opts?: JidToE164Options }): string[] {
const dirs = new Set<string>();
const addDir = (dir?: string | null ) => {
if (!dir) {
return ;
}
dirs.add(resolveUserPath(dir));
};
addDir(params.opts?.authDir);
for (const dir of params.opts?.lidMappingDirs ?? []) {
addDir(dir);
}
addDir(CONFIG_DIR);
addDir(path.join(CONFIG_DIR, "credentials" ));
return [...dirs];
}
function readLidReverseMapping(params: { lid: string; opts?: JidToE164Options }): string | null {
const mappingFilename = `lid-mapping-${params.lid}_reverse.json`;
const mappingDirs = resolveLidMappingDirs({ opts: params.opts });
for (const dir of mappingDirs) {
const mappingPath = path.join(dir, mappingFilename);
try {
const data = fs.readFileSync(mappingPath, "utf8" );
const phone = JSON.parse(data) as string | number | null ;
if (phone === null || phone === undefined) {
continue ;
}
return normalizeE164(String(phone));
} catch {
// next location
}
}
return null ;
}
export function jidToE164(jid: string, opts?: JidToE164Options): string | null {
const match = jid.match(/^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted)$/);
if (match) {
return `+${match[1 ]}`;
}
const lidMatch = jid.match(/^(\d+)(?::\d+)?@(lid|hosted\.lid)$/);
if (!lidMatch) {
return null ;
}
const phone = readLidReverseMapping({
lid: lidMatch[1 ],
opts,
});
if (phone) {
return phone;
}
const shouldLog = opts?.logMissing ?? shouldLogVerbose();
if (shouldLog) {
logVerbose(`LID mapping not found for ${lidMatch[1 ]}; skipping inbound message`);
}
return null ;
}
export async function resolveJidToE164(
jid: string | null | undefined,
opts?: JidToE164Options & { lidLookup?: LidLookup },
): Promise<string | null > {
if (!jid) {
return null ;
}
const direct = jidToE164(jid, opts);
if (direct) {
return direct;
}
if (!/(@lid|@hosted\.lid)$/.test(jid) || !opts?.lidLookup?.getPNForLID) {
return null ;
}
try {
const pnJid = await opts.lidLookup.getPNForLID(jid);
if (!pnJid) {
return null ;
}
return jidToE164(pnJid, opts);
} catch (err) {
if (shouldLogVerbose()) {
logVerbose(`LID mapping lookup failed for ${jid}: ${String(err)}`);
}
return null ;
}
}
export function markdownToWhatsApp(text: string): string {
if (!text) {
return text;
}
const fences: string[] = [];
let result = text.replace(/```[\s\S]*?```/g, (match) => {
fences.push(match);
return `${WHATSAPP_FENCE_PLACEHOLDER}${fences.length - 1 }`;
});
const inlineCodes: string[] = [];
result = result.replace(/`[^`\n]+`/g, (match) => {
inlineCodes.push(match);
return `${WHATSAPP_INLINE_CODE_PLACEHOLDER}${inlineCodes.length - 1 }`;
});
result = result.replace(/\*\*(.+?)\*\*/g, "*$1*" );
result = result.replace(/__(.+?)__/g, "*$1*" );
result = result.replace(/~~(.+?)~~/g, "~$1~" );
result = result.replace(
new RegExp(`${escapeRegExp(WHATSAPP_INLINE_CODE_PLACEHOLDER)}(\\d+)`, "g" ),
(_, idx) => inlineCodes[Number(idx)] ?? "" ,
);
result = result.replace(
new RegExp(`${escapeRegExp(WHATSAPP_FENCE_PLACEHOLDER)}(\\d+)`, "g" ),
(_, idx) => fences[Number(idx)] ?? "" ,
);
return result;
}
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Dauer der Verarbeitung: 0.7 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland