owpenwork 0.1.1

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/dist/config.js ADDED
@@ -0,0 +1,169 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import dotenv from "dotenv";
6
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
7
+ const packageDir = path.resolve(moduleDir, "..");
8
+ dotenv.config({ path: path.join(packageDir, ".env") });
9
+ dotenv.config();
10
+ function parseBoolean(value, fallback) {
11
+ if (value === undefined)
12
+ return fallback;
13
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
14
+ }
15
+ function parseInteger(value) {
16
+ if (!value)
17
+ return undefined;
18
+ const parsed = Number.parseInt(value, 10);
19
+ return Number.isFinite(parsed) ? parsed : undefined;
20
+ }
21
+ function parseList(value) {
22
+ if (!value)
23
+ return [];
24
+ return value
25
+ .split(",")
26
+ .map((item) => item.trim())
27
+ .filter(Boolean);
28
+ }
29
+ function expandHome(value) {
30
+ if (!value.startsWith("~/"))
31
+ return value;
32
+ return path.join(os.homedir(), value.slice(2));
33
+ }
34
+ export function normalizeWhatsAppId(value) {
35
+ const trimmed = value.trim();
36
+ if (!trimmed)
37
+ return trimmed;
38
+ if (trimmed.endsWith("@g.us"))
39
+ return trimmed;
40
+ const base = trimmed.replace(/@s\.whatsapp\.net$/i, "");
41
+ if (base.startsWith("+"))
42
+ return base;
43
+ if (/^\d+$/.test(base))
44
+ return `+${base}`;
45
+ return base;
46
+ }
47
+ function normalizeWhatsAppAllowFrom(list) {
48
+ const set = new Set();
49
+ for (const entry of list) {
50
+ const trimmed = entry.trim();
51
+ if (!trimmed)
52
+ continue;
53
+ if (trimmed === "*") {
54
+ set.add("*");
55
+ continue;
56
+ }
57
+ set.add(normalizeWhatsAppId(trimmed));
58
+ }
59
+ return set;
60
+ }
61
+ function normalizeDmPolicy(value) {
62
+ if (value === "allowlist" || value === "open" || value === "disabled" || value === "pairing") {
63
+ return value;
64
+ }
65
+ return "pairing";
66
+ }
67
+ function resolveConfigPath(dataDir, env) {
68
+ const override = env.OWPENBOT_CONFIG_PATH?.trim();
69
+ if (override)
70
+ return expandHome(override);
71
+ return path.join(dataDir, "owpenbot.json");
72
+ }
73
+ export function readConfigFile(configPath) {
74
+ try {
75
+ const raw = fs.readFileSync(configPath, "utf-8");
76
+ const parsed = JSON.parse(raw);
77
+ return { exists: true, config: parsed };
78
+ }
79
+ catch (error) {
80
+ if (error.code === "ENOENT") {
81
+ return { exists: false, config: { version: 1 } };
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+ export function writeConfigFile(configPath, config) {
87
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
88
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
89
+ }
90
+ function parseAllowlist(env) {
91
+ const allowlist = {
92
+ telegram: new Set(),
93
+ whatsapp: new Set(),
94
+ };
95
+ const shared = parseList(env.ALLOW_FROM);
96
+ for (const entry of shared) {
97
+ if (entry.includes(":")) {
98
+ const [channel, peer] = entry.split(":");
99
+ const normalized = channel.trim().toLowerCase();
100
+ if (normalized === "telegram" || normalized === "whatsapp") {
101
+ if (peer?.trim()) {
102
+ allowlist[normalized].add(peer.trim());
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ allowlist.telegram.add(entry);
108
+ allowlist.whatsapp.add(entry);
109
+ }
110
+ }
111
+ for (const entry of parseList(env.ALLOW_FROM_TELEGRAM)) {
112
+ allowlist.telegram.add(entry);
113
+ }
114
+ for (const entry of parseList(env.ALLOW_FROM_WHATSAPP)) {
115
+ allowlist.whatsapp.add(entry);
116
+ }
117
+ return allowlist;
118
+ }
119
+ export function loadConfig(env = process.env, options = {}) {
120
+ const requireOpencode = options.requireOpencode ?? false;
121
+ const opencodeDirectory = env.OPENCODE_DIRECTORY?.trim() ?? "";
122
+ if (!opencodeDirectory && requireOpencode) {
123
+ throw new Error("OPENCODE_DIRECTORY is required");
124
+ }
125
+ const resolvedDirectory = opencodeDirectory || process.cwd();
126
+ const dataDir = expandHome(env.OWPENBOT_DATA_DIR ?? "~/.owpenbot");
127
+ const dbPath = expandHome(env.OWPENBOT_DB_PATH ?? path.join(dataDir, "owpenbot.db"));
128
+ const configPath = resolveConfigPath(dataDir, env);
129
+ const { config: configFile } = readConfigFile(configPath);
130
+ const whatsappFile = configFile.channels?.whatsapp ?? {};
131
+ const whatsappAccountId = env.WHATSAPP_ACCOUNT_ID?.trim() || "default";
132
+ const accountAuthDir = whatsappFile.accounts?.[whatsappAccountId]?.authDir;
133
+ const whatsappAuthDir = expandHome(env.WHATSAPP_AUTH_DIR ??
134
+ accountAuthDir ??
135
+ path.join(dataDir, "credentials", "whatsapp", whatsappAccountId));
136
+ const dmPolicy = normalizeDmPolicy(env.WHATSAPP_DM_POLICY?.trim().toLowerCase() ?? whatsappFile.dmPolicy);
137
+ const selfChatMode = parseBoolean(env.WHATSAPP_SELF_CHAT, whatsappFile.selfChatMode ?? false);
138
+ const envAllowlist = parseAllowlist(env);
139
+ const fileAllowFrom = normalizeWhatsAppAllowFrom(whatsappFile.allowFrom ?? []);
140
+ const envAllowFrom = normalizeWhatsAppAllowFrom(envAllowlist.whatsapp.size ? [...envAllowlist.whatsapp] : []);
141
+ const whatsappAllowFrom = new Set([...fileAllowFrom, ...envAllowFrom]);
142
+ const toolOutputLimit = parseInteger(env.TOOL_OUTPUT_LIMIT) ?? 1200;
143
+ const permissionMode = env.PERMISSION_MODE?.toLowerCase() === "deny" ? "deny" : "allow";
144
+ return {
145
+ configPath,
146
+ configFile,
147
+ opencodeUrl: env.OPENCODE_URL?.trim() ?? "http://127.0.0.1:4096",
148
+ opencodeDirectory: resolvedDirectory,
149
+ opencodeUsername: env.OPENCODE_SERVER_USERNAME?.trim() || undefined,
150
+ opencodePassword: env.OPENCODE_SERVER_PASSWORD?.trim() || undefined,
151
+ telegramToken: env.TELEGRAM_BOT_TOKEN?.trim() || undefined,
152
+ telegramEnabled: parseBoolean(env.TELEGRAM_ENABLED, Boolean(env.TELEGRAM_BOT_TOKEN?.trim())),
153
+ whatsappAuthDir,
154
+ whatsappAccountId,
155
+ whatsappDmPolicy: dmPolicy,
156
+ whatsappAllowFrom,
157
+ whatsappSelfChatMode: selfChatMode,
158
+ whatsappEnabled: parseBoolean(env.WHATSAPP_ENABLED, true),
159
+ dataDir,
160
+ dbPath,
161
+ allowlist: envAllowlist,
162
+ toolUpdatesEnabled: parseBoolean(env.TOOL_UPDATES_ENABLED, false),
163
+ groupsEnabled: parseBoolean(env.GROUPS_ENABLED, false),
164
+ permissionMode,
165
+ toolOutputLimit,
166
+ healthPort: parseInteger(env.OWPENBOT_HEALTH_PORT),
167
+ logLevel: env.LOG_LEVEL?.trim() || "info",
168
+ };
169
+ }
package/dist/db.js ADDED
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ export class BridgeStore {
5
+ dbPath;
6
+ db;
7
+ constructor(dbPath) {
8
+ this.dbPath = dbPath;
9
+ this.ensureDir();
10
+ this.db = new Database(dbPath);
11
+ this.db.pragma("journal_mode = WAL");
12
+ this.db.exec(`
13
+ CREATE TABLE IF NOT EXISTS sessions (
14
+ channel TEXT NOT NULL,
15
+ peer_id TEXT NOT NULL,
16
+ session_id TEXT NOT NULL,
17
+ created_at INTEGER NOT NULL,
18
+ updated_at INTEGER NOT NULL,
19
+ PRIMARY KEY (channel, peer_id)
20
+ );
21
+ CREATE TABLE IF NOT EXISTS allowlist (
22
+ channel TEXT NOT NULL,
23
+ peer_id TEXT NOT NULL,
24
+ created_at INTEGER NOT NULL,
25
+ PRIMARY KEY (channel, peer_id)
26
+ );
27
+ CREATE TABLE IF NOT EXISTS settings (
28
+ key TEXT PRIMARY KEY,
29
+ value TEXT NOT NULL
30
+ );
31
+ CREATE TABLE IF NOT EXISTS pairing_requests (
32
+ channel TEXT NOT NULL,
33
+ peer_id TEXT NOT NULL,
34
+ code TEXT NOT NULL,
35
+ created_at INTEGER NOT NULL,
36
+ expires_at INTEGER NOT NULL,
37
+ PRIMARY KEY (channel, peer_id)
38
+ );
39
+ `);
40
+ }
41
+ ensureDir() {
42
+ const dir = path.dirname(this.dbPath);
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ getSession(channel, peerId) {
46
+ const stmt = this.db.prepare("SELECT channel, peer_id, session_id, created_at, updated_at FROM sessions WHERE channel = ? AND peer_id = ?");
47
+ const row = stmt.get(channel, peerId);
48
+ return row ?? null;
49
+ }
50
+ upsertSession(channel, peerId, sessionId) {
51
+ const now = Date.now();
52
+ const stmt = this.db.prepare(`INSERT INTO sessions (channel, peer_id, session_id, created_at, updated_at)
53
+ VALUES (?, ?, ?, ?, ?)
54
+ ON CONFLICT(channel, peer_id) DO UPDATE SET session_id = excluded.session_id, updated_at = excluded.updated_at`);
55
+ stmt.run(channel, peerId, sessionId, now, now);
56
+ }
57
+ isAllowed(channel, peerId) {
58
+ const stmt = this.db.prepare("SELECT channel, peer_id, created_at FROM allowlist WHERE channel = ? AND peer_id = ?");
59
+ return Boolean(stmt.get(channel, peerId));
60
+ }
61
+ allowPeer(channel, peerId) {
62
+ const now = Date.now();
63
+ const stmt = this.db.prepare(`INSERT INTO allowlist (channel, peer_id, created_at)
64
+ VALUES (?, ?, ?)
65
+ ON CONFLICT(channel, peer_id) DO UPDATE SET created_at = excluded.created_at`);
66
+ stmt.run(channel, peerId, now);
67
+ }
68
+ seedAllowlist(channel, peers) {
69
+ const insert = this.db.prepare(`INSERT INTO allowlist (channel, peer_id, created_at)
70
+ VALUES (?, ?, ?)
71
+ ON CONFLICT(channel, peer_id) DO NOTHING`);
72
+ const now = Date.now();
73
+ const transaction = this.db.transaction((items) => {
74
+ for (const peer of items) {
75
+ insert.run(channel, peer, now);
76
+ }
77
+ });
78
+ transaction(peers);
79
+ }
80
+ listPairingRequests(channel) {
81
+ const now = Date.now();
82
+ const stmt = channel
83
+ ? this.db.prepare("SELECT channel, peer_id, code, created_at, expires_at FROM pairing_requests WHERE channel = ? AND expires_at > ? ORDER BY created_at ASC")
84
+ : this.db.prepare("SELECT channel, peer_id, code, created_at, expires_at FROM pairing_requests WHERE expires_at > ? ORDER BY created_at ASC");
85
+ const rows = (channel ? stmt.all(channel, now) : stmt.all(now));
86
+ return rows;
87
+ }
88
+ getPairingRequest(channel, peerId) {
89
+ const now = Date.now();
90
+ const stmt = this.db.prepare("SELECT channel, peer_id, code, created_at, expires_at FROM pairing_requests WHERE channel = ? AND peer_id = ? AND expires_at > ?");
91
+ const row = stmt.get(channel, peerId, now);
92
+ return row ?? null;
93
+ }
94
+ createPairingRequest(channel, peerId, code, ttlMs) {
95
+ const now = Date.now();
96
+ const expiresAt = now + ttlMs;
97
+ const stmt = this.db.prepare(`INSERT INTO pairing_requests (channel, peer_id, code, created_at, expires_at)
98
+ VALUES (?, ?, ?, ?, ?)
99
+ ON CONFLICT(channel, peer_id) DO UPDATE SET code = excluded.code, created_at = excluded.created_at, expires_at = excluded.expires_at`);
100
+ stmt.run(channel, peerId, code, now, expiresAt);
101
+ }
102
+ approvePairingRequest(channel, code) {
103
+ const now = Date.now();
104
+ const select = this.db.prepare("SELECT channel, peer_id, code, created_at, expires_at FROM pairing_requests WHERE channel = ? AND code = ? AND expires_at > ?");
105
+ const row = select.get(channel, code, now);
106
+ if (!row)
107
+ return null;
108
+ const del = this.db.prepare("DELETE FROM pairing_requests WHERE channel = ? AND peer_id = ?");
109
+ del.run(channel, row.peer_id);
110
+ return row;
111
+ }
112
+ denyPairingRequest(channel, code) {
113
+ const stmt = this.db.prepare("DELETE FROM pairing_requests WHERE channel = ? AND code = ?");
114
+ const result = stmt.run(channel, code);
115
+ return result.changes > 0;
116
+ }
117
+ prunePairingRequests() {
118
+ const now = Date.now();
119
+ const stmt = this.db.prepare("DELETE FROM pairing_requests WHERE expires_at <= ?");
120
+ stmt.run(now);
121
+ }
122
+ getSetting(key) {
123
+ const stmt = this.db.prepare("SELECT value FROM settings WHERE key = ?");
124
+ const row = stmt.get(key);
125
+ return row?.value ?? null;
126
+ }
127
+ setSetting(key, value) {
128
+ const stmt = this.db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
129
+ stmt.run(key, value);
130
+ }
131
+ close() {
132
+ this.db.close();
133
+ }
134
+ }
package/dist/events.js ADDED
@@ -0,0 +1,11 @@
1
+ export function normalizeEvent(raw) {
2
+ if (!raw)
3
+ return null;
4
+ if (typeof raw.type === "string") {
5
+ return { type: raw.type, properties: raw.properties };
6
+ }
7
+ if (raw.payload && typeof raw.payload.type === "string") {
8
+ return { type: raw.payload.type, properties: raw.payload.properties };
9
+ }
10
+ return null;
11
+ }
package/dist/health.js ADDED
@@ -0,0 +1,21 @@
1
+ import http from "node:http";
2
+ export function startHealthServer(port, getStatus, logger) {
3
+ const server = http.createServer((req, res) => {
4
+ if (!req.url || req.url === "/health") {
5
+ const snapshot = getStatus();
6
+ res.writeHead(snapshot.ok ? 200 : 503, {
7
+ "Content-Type": "application/json",
8
+ });
9
+ res.end(JSON.stringify(snapshot));
10
+ return;
11
+ }
12
+ res.writeHead(404, { "Content-Type": "application/json" });
13
+ res.end(JSON.stringify({ ok: false, error: "Not found" }));
14
+ });
15
+ server.listen(port, "0.0.0.0", () => {
16
+ logger.info({ port }, "health server listening");
17
+ });
18
+ return () => {
19
+ server.close();
20
+ };
21
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,7 @@
1
+ import pino from "pino";
2
+ export function createLogger(level) {
3
+ return pino({
4
+ level,
5
+ base: undefined,
6
+ });
7
+ }
@@ -0,0 +1,34 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
3
+ export function createClient(config) {
4
+ const headers = {};
5
+ if (config.opencodeUsername && config.opencodePassword) {
6
+ const token = Buffer.from(`${config.opencodeUsername}:${config.opencodePassword}`).toString("base64");
7
+ headers.Authorization = `Basic ${token}`;
8
+ }
9
+ return createOpencodeClient({
10
+ baseUrl: config.opencodeUrl,
11
+ directory: config.opencodeDirectory,
12
+ headers: Object.keys(headers).length ? headers : undefined,
13
+ responseStyle: "data",
14
+ throwOnError: true,
15
+ });
16
+ }
17
+ export function buildPermissionRules(mode) {
18
+ if (mode === "deny") {
19
+ return [
20
+ {
21
+ permission: "*",
22
+ pattern: "*",
23
+ action: "deny",
24
+ },
25
+ ];
26
+ }
27
+ return [
28
+ {
29
+ permission: "*",
30
+ pattern: "*",
31
+ action: "allow",
32
+ },
33
+ ];
34
+ }
@@ -0,0 +1,45 @@
1
+ import { Bot } from "grammy";
2
+ const MAX_TEXT_LENGTH = 4096;
3
+ export function createTelegramAdapter(config, logger, onMessage) {
4
+ if (!config.telegramToken) {
5
+ throw new Error("TELEGRAM_BOT_TOKEN is required for Telegram adapter");
6
+ }
7
+ const bot = new Bot(config.telegramToken);
8
+ bot.catch((err) => {
9
+ logger.error({ error: err.error }, "telegram bot error");
10
+ });
11
+ bot.on("message", async (ctx) => {
12
+ const msg = ctx.message;
13
+ if (!msg?.chat)
14
+ return;
15
+ const chatType = msg.chat.type;
16
+ const isGroup = chatType === "group" || chatType === "supergroup" || chatType === "channel";
17
+ if (isGroup && !config.groupsEnabled) {
18
+ return;
19
+ }
20
+ const text = msg.text ?? msg.caption ?? "";
21
+ if (!text.trim())
22
+ return;
23
+ await onMessage({
24
+ channel: "telegram",
25
+ peerId: String(msg.chat.id),
26
+ text,
27
+ raw: msg,
28
+ });
29
+ });
30
+ return {
31
+ name: "telegram",
32
+ maxTextLength: MAX_TEXT_LENGTH,
33
+ async start() {
34
+ await bot.start();
35
+ logger.info("telegram adapter started");
36
+ },
37
+ async stop() {
38
+ bot.stop();
39
+ logger.info("telegram adapter stopped");
40
+ },
41
+ async sendText(peerId, text) {
42
+ await bot.api.sendMessage(Number(peerId), text);
43
+ },
44
+ };
45
+ }
package/dist/text.js ADDED
@@ -0,0 +1,41 @@
1
+ export function chunkText(input, limit) {
2
+ if (input.length <= limit)
3
+ return [input];
4
+ const chunks = [];
5
+ let current = "";
6
+ for (const line of input.split(/\n/)) {
7
+ if ((current + line).length + 1 > limit) {
8
+ if (current)
9
+ chunks.push(current.trimEnd());
10
+ current = "";
11
+ }
12
+ if (line.length > limit) {
13
+ for (let i = 0; i < line.length; i += limit) {
14
+ const slice = line.slice(i, i + limit);
15
+ if (slice.length)
16
+ chunks.push(slice);
17
+ }
18
+ continue;
19
+ }
20
+ current += current ? `\n${line}` : line;
21
+ }
22
+ if (current.trim().length)
23
+ chunks.push(current.trimEnd());
24
+ return chunks.length ? chunks : [input];
25
+ }
26
+ export function truncateText(text, limit) {
27
+ if (text.length <= limit)
28
+ return text;
29
+ return `${text.slice(0, Math.max(0, limit - 1))}…`;
30
+ }
31
+ export function formatInputSummary(input) {
32
+ const entries = Object.entries(input);
33
+ if (!entries.length)
34
+ return "";
35
+ try {
36
+ return JSON.stringify(input);
37
+ }
38
+ catch {
39
+ return entries.map(([key, value]) => `${key}=${String(value)}`).join(", ");
40
+ }
41
+ }
@@ -0,0 +1,166 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DisconnectReason, fetchLatestBaileysVersion, isJidGroup, makeCacheableSignalKeyStore, makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys";
4
+ import qrcode from "qrcode-terminal";
5
+ const MAX_TEXT_LENGTH = 3800;
6
+ function extractText(message) {
7
+ const content = message.message;
8
+ if (!content)
9
+ return "";
10
+ return (content.conversation ||
11
+ content.extendedTextMessage?.text ||
12
+ content.imageMessage?.caption ||
13
+ content.videoMessage?.caption ||
14
+ content.documentMessage?.caption ||
15
+ "");
16
+ }
17
+ function ensureDir(dir) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
21
+ let socket = null;
22
+ let stopped = false;
23
+ const log = logger.child({ channel: "whatsapp" });
24
+ const authDir = path.resolve(config.whatsappAuthDir);
25
+ ensureDir(authDir);
26
+ async function connect() {
27
+ const { state, saveCreds } = await useMultiFileAuthState(authDir);
28
+ const { version } = await fetchLatestBaileysVersion();
29
+ const sock = makeWASocket({
30
+ auth: {
31
+ creds: state.creds,
32
+ keys: makeCacheableSignalKeyStore(state.keys, log),
33
+ },
34
+ version,
35
+ logger: log,
36
+ printQRInTerminal: false,
37
+ syncFullHistory: false,
38
+ markOnlineOnConnect: false,
39
+ browser: ["owpenbot", "cli", "0.1.0"],
40
+ });
41
+ sock.ev.on("creds.update", saveCreds);
42
+ sock.ev.on("connection.update", (update) => {
43
+ if (update.qr && opts.printQr) {
44
+ qrcode.generate(update.qr, { small: true });
45
+ log.info("scan the QR code to connect WhatsApp");
46
+ }
47
+ if (update.connection === "open") {
48
+ log.info("whatsapp connected");
49
+ }
50
+ if (update.connection === "close") {
51
+ const lastDisconnect = update.lastDisconnect;
52
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
53
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
54
+ if (shouldReconnect && !stopped) {
55
+ log.warn("whatsapp connection closed, reconnecting");
56
+ void connect();
57
+ }
58
+ else if (!shouldReconnect) {
59
+ log.warn("whatsapp logged out, run 'owpenbot whatsapp login'");
60
+ }
61
+ }
62
+ });
63
+ sock.ev.on("messages.upsert", async ({ messages }) => {
64
+ for (const msg of messages) {
65
+ if (!msg.message)
66
+ continue;
67
+ const fromMe = Boolean(msg.key.fromMe);
68
+ if (fromMe && !config.whatsappSelfChatMode)
69
+ continue;
70
+ const peerId = msg.key.remoteJid;
71
+ if (!peerId)
72
+ continue;
73
+ if (isJidGroup(peerId) && !config.groupsEnabled) {
74
+ continue;
75
+ }
76
+ const text = extractText(msg);
77
+ if (!text.trim())
78
+ continue;
79
+ await onMessage({
80
+ channel: "whatsapp",
81
+ peerId,
82
+ text,
83
+ raw: msg,
84
+ fromMe,
85
+ });
86
+ }
87
+ });
88
+ socket = sock;
89
+ }
90
+ return {
91
+ name: "whatsapp",
92
+ maxTextLength: MAX_TEXT_LENGTH,
93
+ async start() {
94
+ await connect();
95
+ },
96
+ async stop() {
97
+ stopped = true;
98
+ if (socket) {
99
+ socket.end(undefined);
100
+ socket = null;
101
+ }
102
+ },
103
+ async sendText(peerId, text) {
104
+ if (!socket)
105
+ throw new Error("WhatsApp socket not initialized");
106
+ await socket.sendMessage(peerId, { text });
107
+ },
108
+ };
109
+ }
110
+ export async function loginWhatsApp(config, logger) {
111
+ const authDir = path.resolve(config.whatsappAuthDir);
112
+ ensureDir(authDir);
113
+ const log = logger.child({ channel: "whatsapp" });
114
+ const { state, saveCreds } = await useMultiFileAuthState(authDir);
115
+ const { version } = await fetchLatestBaileysVersion();
116
+ await new Promise((resolve) => {
117
+ let finished = false;
118
+ const sock = makeWASocket({
119
+ auth: {
120
+ creds: state.creds,
121
+ keys: makeCacheableSignalKeyStore(state.keys, log),
122
+ },
123
+ version,
124
+ logger: log,
125
+ printQRInTerminal: false,
126
+ syncFullHistory: false,
127
+ markOnlineOnConnect: false,
128
+ browser: ["owpenbot", "cli", "0.1.0"],
129
+ });
130
+ const finish = (reason) => {
131
+ if (finished)
132
+ return;
133
+ finished = true;
134
+ log.info({ reason }, "whatsapp login finished");
135
+ sock.end(undefined);
136
+ resolve();
137
+ };
138
+ sock.ev.on("creds.update", async () => {
139
+ await saveCreds();
140
+ if (state.creds?.registered) {
141
+ finish("creds.registered");
142
+ }
143
+ });
144
+ sock.ev.on("connection.update", (update) => {
145
+ if (update.qr) {
146
+ qrcode.generate(update.qr, { small: true });
147
+ log.info("scan the QR code to connect WhatsApp");
148
+ }
149
+ if (update.connection === "open") {
150
+ finish("connection.open");
151
+ }
152
+ if (update.connection === "close" && state.creds?.registered) {
153
+ finish("connection.close.registered");
154
+ }
155
+ });
156
+ });
157
+ }
158
+ export function unpairWhatsApp(config, logger) {
159
+ const authDir = path.resolve(config.whatsappAuthDir);
160
+ if (!fs.existsSync(authDir)) {
161
+ logger.info({ authDir }, "whatsapp auth directory not found");
162
+ return;
163
+ }
164
+ fs.rmSync(authDir, { recursive: true, force: true });
165
+ logger.info({ authDir }, "whatsapp auth cleared; run owpenbot to re-pair");
166
+ }