diff --git a/.claude/skills/add-whatsapp-v2/SKILL.md b/.claude/skills/add-whatsapp-v2/SKILL.md index b9ee49e..8d90b62 100644 --- a/.claude/skills/add-whatsapp-v2/SKILL.md +++ b/.claude/skills/add-whatsapp-v2/SKILL.md @@ -17,8 +17,9 @@ Skip to **Credentials** if all of these are already in place: - `src/channels/whatsapp.ts` exists - `src/channels/index.ts` contains `import './whatsapp.js';` -- `setup/whatsapp-auth.ts` exists and `setup/index.ts`'s `STEPS` map contains `'whatsapp-auth':` -- `@whiskeysockets/baileys`, `qrcode` are listed in `package.json` dependencies +- `setup/whatsapp-auth.ts` and `setup/groups.ts` both exist +- `setup/index.ts`'s `STEPS` map contains both `'whatsapp-auth':` and `groups:` +- `@whiskeysockets/baileys`, `qrcode`, `pino` are listed in `package.json` dependencies Otherwise continue. Every step below is safe to re-run. @@ -28,11 +29,12 @@ Otherwise continue. Every step below is safe to re-run. git fetch origin channels ``` -### 2. Copy the adapter and setup step +### 2. Copy the adapter and setup steps ```bash git show origin/channels:src/channels/whatsapp.ts > src/channels/whatsapp.ts git show origin/channels:setup/whatsapp-auth.ts > setup/whatsapp-auth.ts +git show origin/channels:setup/groups.ts > setup/groups.ts ``` ### 3. Append the self-registration import @@ -43,22 +45,21 @@ Append to `src/channels/index.ts` (skip if already present): import './whatsapp.js'; ``` -### 4. Register the setup step +### 4. Register the setup steps -In `setup/index.ts`, add this entry to the `STEPS` map (skip if already present): +In `setup/index.ts`, add these entries to the `STEPS` map (skip lines already present): ```typescript +groups: () => import('./groups.js'), 'whatsapp-auth': () => import('./whatsapp-auth.js'), ``` ### 5. Install the adapter packages (pinned) ```bash -pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 +pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0 ``` -`pino` is already a v2 trunk dependency — no separate install needed. - ### 6. Build ```bash diff --git a/package.json b/package.json index 883ff3e..e9d2aca 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "@onecli-sh/sdk": "^0.3.1", "better-sqlite3": "11.10.0", "chat": "^4.24.0", - "cron-parser": "5.5.0", - "pino": "^9.6.0" + "cron-parser": "5.5.0" }, "devDependencies": { "@eslint/js": "^9.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7cb770..f10d6a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: cron-parser: specifier: 5.5.0 version: 5.5.0 - pino: - specifier: ^9.6.0 - version: 9.14.0 devDependencies: '@eslint/js': specifier: ^9.35.0 @@ -298,9 +295,6 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - '@pinojs/redact@0.4.0': - resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -550,10 +544,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - atomic-sleep@1.0.0: - resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} - engines: {node: '>=8.0.0'} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1129,10 +1119,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - on-exit-leak-free@2.1.2: - resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} - engines: {node: '>=14.0.0'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1170,16 +1156,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - - pino-std-serializers@7.1.0: - resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - - pino@9.14.0: - resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} - hasBin: true - postcss@8.5.9: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} @@ -1199,9 +1175,6 @@ packages: engines: {node: '>=14'} hasBin: true - process-warning@5.0.0: - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -1209,9 +1182,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -1220,10 +1190,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -1251,10 +1217,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1277,17 +1239,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - sonic-boom@4.2.1: - resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1316,9 +1271,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1674,8 +1626,6 @@ snapshots: '@oxc-project/types@0.124.0': {} - '@pinojs/redact@0.4.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true @@ -1920,8 +1870,6 @@ snapshots: assertion-error@2.0.1: {} - atomic-sleep@1.0.0: {} - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -2634,8 +2582,6 @@ snapshots: obug@2.1.1: {} - on-exit-leak-free@2.1.2: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -2671,26 +2617,6 @@ snapshots: picomatch@4.0.4: {} - pino-abstract-transport@2.0.0: - dependencies: - split2: 4.2.0 - - pino-std-serializers@7.1.0: {} - - pino@9.14.0: - dependencies: - '@pinojs/redact': 0.4.0 - atomic-sleep: 1.0.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 - pino-std-serializers: 7.1.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.1 - thread-stream: 3.1.0 - postcss@8.5.9: dependencies: nanoid: 3.3.11 @@ -2716,8 +2642,6 @@ snapshots: prettier@3.8.2: {} - process-warning@5.0.0: {} - pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -2725,8 +2649,6 @@ snapshots: punycode@2.3.1: {} - quick-format-unescaped@4.0.4: {} - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -2740,8 +2662,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - real-require@0.2.0: {} - remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -2797,8 +2717,6 @@ snapshots: safe-buffer@5.2.1: {} - safe-stable-stringify@2.5.0: {} - semver@7.7.4: {} shebang-command@2.0.0: @@ -2817,14 +2735,8 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - sonic-boom@4.2.1: - dependencies: - atomic-sleep: 1.0.0 - source-map-js@1.2.1: {} - split2@4.2.0: {} - stackback@0.0.2: {} std-env@4.0.0: {} @@ -2856,10 +2768,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - thread-stream@3.1.0: - dependencies: - real-require: 0.2.0 - tinybench@2.9.0: {} tinyexec@1.1.1: {} diff --git a/setup/groups.ts b/setup/groups.ts deleted file mode 100644 index 4c71e86..0000000 --- a/setup/groups.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Step: groups — Fetch group metadata from messaging platforms, write to DB. - * WhatsApp requires an upfront sync (Baileys groupFetchAllParticipating). - * Other channels discover group names at runtime — this step auto-skips for them. - * Replaces 05-sync-groups.sh + 05b-list-groups.sh - */ -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -import Database from 'better-sqlite3'; - -import { STORE_DIR } from '../src/config.js'; -import { log } from '../src/log.js'; -import { emitStatus } from './status.js'; - -function parseArgs(args: string[]): { list: boolean; limit: number } { - let list = false; - let limit = 30; - for (let i = 0; i < args.length; i++) { - if (args[i] === '--list') list = true; - if (args[i] === '--limit' && args[i + 1]) { - limit = parseInt(args[i + 1], 10); - i++; - } - } - return { list, limit }; -} - -export async function run(args: string[]): Promise { - const projectRoot = process.cwd(); - const { list, limit } = parseArgs(args); - - if (list) { - await listGroups(limit); - return; - } - - await syncGroups(projectRoot); -} - -async function listGroups(limit: number): Promise { - const dbPath = path.join(STORE_DIR, 'messages.db'); - - if (!fs.existsSync(dbPath)) { - console.error('ERROR: database not found'); - process.exit(1); - } - - const db = new Database(dbPath, { readonly: true }); - const rows = db - .prepare( - `SELECT jid, name FROM chats - WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__' AND name <> jid - ORDER BY last_message_time DESC - LIMIT ?`, - ) - .all(limit) as Array<{ jid: string; name: string }>; - db.close(); - - for (const row of rows) { - console.log(`${row.jid}|${row.name}`); - } -} - -async function syncGroups(projectRoot: string): Promise { - // Only WhatsApp needs an upfront group sync; other channels resolve names at runtime. - // Detect WhatsApp by checking for auth credentials on disk. - const authDir = path.join(projectRoot, 'store', 'auth'); - const hasWhatsAppAuth = - fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0; - - if (!hasWhatsAppAuth) { - log.info('WhatsApp auth not found — skipping group sync'); - emitStatus('SYNC_GROUPS', { - BUILD: 'skipped', - SYNC: 'skipped', - GROUPS_IN_DB: 0, - REASON: 'whatsapp_not_configured', - STATUS: 'success', - LOG: 'logs/setup.log', - }); - return; - } - - // Build TypeScript first - log.info('Building TypeScript'); - let buildOk = false; - try { - execSync('pnpm run build', { - cwd: projectRoot, - stdio: ['ignore', 'pipe', 'pipe'], - }); - buildOk = true; - log.info('Build succeeded'); - } catch { - log.error('Build failed'); - emitStatus('SYNC_GROUPS', { - BUILD: 'failed', - SYNC: 'skipped', - GROUPS_IN_DB: 0, - STATUS: 'failed', - ERROR: 'build_failed', - LOG: 'logs/setup.log', - }); - process.exit(1); - } - - // Run sync script via a temp file to avoid shell escaping issues with node -e - log.info('Fetching group metadata'); - let syncOk = false; - try { - const syncScript = ` -import makeWASocket, { useMultiFileAuthState, makeCacheableSignalKeyStore, Browsers } from '@whiskeysockets/baileys'; -import pino from 'pino'; -import path from 'path'; -import fs from 'fs'; -import Database from 'better-sqlite3'; - -const logger = pino({ level: 'silent' }); -const authDir = path.join('store', 'auth'); -const dbPath = path.join('store', 'messages.db'); - -if (!fs.existsSync(authDir)) { - console.error('NO_AUTH'); - process.exit(1); -} - -const db = new Database(dbPath); -db.pragma('journal_mode = WAL'); -db.exec('CREATE TABLE IF NOT EXISTS chats (jid TEXT PRIMARY KEY, name TEXT, last_message_time TEXT)'); - -const upsert = db.prepare( - 'INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?) ON CONFLICT(jid) DO UPDATE SET name = excluded.name' -); - -const { state, saveCreds } = await useMultiFileAuthState(authDir); - -const sock = makeWASocket({ - auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) }, - printQRInTerminal: false, - logger, - browser: Browsers.macOS('Chrome'), -}); - -const timeout = setTimeout(() => { - console.error('TIMEOUT'); - process.exit(1); -}, 30000); - -sock.ev.on('creds.update', saveCreds); - -sock.ev.on('connection.update', async (update) => { - if (update.connection === 'open') { - try { - const groups = await sock.groupFetchAllParticipating(); - const now = new Date().toISOString(); - let count = 0; - for (const [jid, metadata] of Object.entries(groups)) { - if (metadata.subject) { - upsert.run(jid, metadata.subject, now); - count++; - } - } - console.log('SYNCED:' + count); - } catch (err) { - console.error('FETCH_ERROR:' + err.message); - } finally { - clearTimeout(timeout); - sock.end(undefined); - db.close(); - process.exit(0); - } - } else if (update.connection === 'close') { - clearTimeout(timeout); - console.error('CONNECTION_CLOSED'); - process.exit(1); - } -}); -`; - - const tmpScript = path.join(projectRoot, '.tmp-group-sync.mjs'); - fs.writeFileSync(tmpScript, syncScript, 'utf-8'); - try { - const output = execSync(`node ${tmpScript}`, { - cwd: projectRoot, - encoding: 'utf-8', - timeout: 45000, - stdio: ['ignore', 'pipe', 'pipe'], - }); - syncOk = output.includes('SYNCED:'); - log.info('Sync output', { output: output.trim() }); - } finally { - try { fs.unlinkSync(tmpScript); } catch { /* ignore cleanup errors */ } - } - } catch (err) { - log.error('Sync failed', { err }); - } - - // Count groups in DB using better-sqlite3 (no sqlite3 CLI) - let groupsInDb = 0; - const dbPath = path.join(STORE_DIR, 'messages.db'); - if (fs.existsSync(dbPath)) { - try { - const db = new Database(dbPath, { readonly: true }); - const row = db - .prepare( - "SELECT COUNT(*) as count FROM chats WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__'", - ) - .get() as { count: number }; - groupsInDb = row.count; - db.close(); - } catch { - // DB may not exist yet - } - } - - const status = syncOk ? 'success' : 'failed'; - - emitStatus('SYNC_GROUPS', { - BUILD: buildOk ? 'success' : 'failed', - SYNC: syncOk ? 'success' : 'failed', - GROUPS_IN_DB: groupsInDb, - STATUS: status, - LOG: 'logs/setup.log', - }); - - if (status === 'failed') process.exit(1); -} diff --git a/setup/index.ts b/setup/index.ts index a65f402..e3b9dd7 100644 --- a/setup/index.ts +++ b/setup/index.ts @@ -12,7 +12,6 @@ const STEPS: Record< timezone: () => import('./timezone.js'), environment: () => import('./environment.js'), container: () => import('./container.js'), - groups: () => import('./groups.js'), register: () => import('./register.js'), mounts: () => import('./mounts.js'), service: () => import('./service.js'),