const express = require('express'); const { remote } = require('webdriverio'); const APPIUM_HOST = process.env.APPIUM_HOST || '127.0.0.1'; const APPIUM_PORT = parseInt(process.env.APPIUM_PORT || '4723', 10); const PORT = parseInt(process.env.PORT || '3010', 10); const app = express(); app.use(express.json({ limit: '10mb' })); let _driver = null; async function getDriver() { if (_driver) { try { await _driver.getSession(); return _driver; } catch { try { await _driver.deleteSession(); } catch {} _driver = null; } } _driver = await remote({ hostname: APPIUM_HOST, port: APPIUM_PORT, capabilities: { platformName: 'Android', 'appium:automationName': 'UiAutomator2', 'appium:deviceName': 'Android', 'appium:noReset': true, 'appium:newCommandTimeout': 120, }, logLevel: 'warn', }); return _driver; } async function retry(fn, attempts = 3, delay = 2000) { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (e) { if (i === attempts - 1) throw e; await new Promise((r) => setTimeout(r, delay)); } } } function uia(selector) { return `android=new UiSelector().${selector}`; } function find(d, selector, timeout = 5000) { return d.$(selector).waitForExist({ timeout }).then(() => d.$(selector), () => null); } async function ensureWhatsApp(d) { const pkg = await d.getCurrentPackage(); if (pkg !== 'com.whatsapp') { await d.startActivity('com.whatsapp', 'com.whatsapp.HomeActivity'); await d.pause(2000); } } async function goHome(d) { await retry(async () => { await d.startActivity('com.whatsapp', 'com.whatsapp.HomeActivity'); await d.pause(1500); }, 2, 1000); } async function openChat(d, query) { await goHome(d); const searchBar = await find( d, uia('resourceId("com.whatsapp:id/search_bar_inner_layout")'), 3000 ); if (!searchBar) throw new Error('Could not find search bar'); await searchBar.click(); await d.pause(2000); const searchInput = await find(d, uia('resourceId("com.whatsapp:id/search_input")'), 3000); if (!searchInput) throw new Error('Could not find search input'); await searchInput.clearValue(); await d.pause(300); await searchInput.setValue(query); await d.pause(2000); // Find and click the matching chat row const chats = await d.$$(uia('resourceId("com.whatsapp:id/contact_row_container")')); for (const row of chats) { const nameEl = await row.$( uia('resourceId("com.whatsapp:id/conversations_row_contact_name")') ); const name = await nameEl.getText().catch(() => ''); if (name.toLowerCase().includes(query.toLowerCase())) { await row.click(); await d.pause(1500); return; } } // If no match by name, try clicking first result if (chats.length > 0) { await chats[0].click(); await d.pause(1500); return; } throw new Error(`No chat found for "${query}"`); } // ── Debug ───────────────────────────────────────────────────────── // ── Endpoints ───────────────────────────────────────────────────── app.get('/health', async (_req, res) => { try { const d = await getDriver(); const session = await d.getSession(); res.json({ status: 'ok', sessionId: d.sessionId }); } catch (e) { res.status(503).json({ status: 'error', message: e.message }); } }); app.get('/client/getChats/:session', async (req, res) => { try { const d = await getDriver(); await ensureWhatsApp(d); await goHome(d); const rows = await d.$$(uia('resourceId("com.whatsapp:id/contact_row_container")')); const chats = []; for (const row of rows) { try { const nameEl = await row.$( uia('resourceId("com.whatsapp:id/conversations_row_contact_name")') ); const name = await nameEl.getText(); const msgEl = await row.$(uia('resourceId("com.whatsapp:id/single_msg_tv")')); let lastMessage = ''; if (await msgEl.isExisting()) { lastMessage = await msgEl.getText(); } else { const msgFromEl = await row.$(uia('resourceId("com.whatsapp:id/msg_from_tv")')); if (await msgFromEl.isExisting()) lastMessage = await msgFromEl.getText(); } const timeEl = await row.$( uia('resourceId("com.whatsapp:id/conversations_row_date")') ); const timestamp = (await timeEl.isExisting()) ? await timeEl.getText() : ''; const unreadEl = await row.$( uia('resourceId("com.whatsapp:id/conversations_row_unread_indicator")') ); let unread = 0; if (await unreadEl.isExisting()) { const desc = await unreadEl.getAttribute('content-desc'); const m = desc ? desc.match(/(\d+)/) : null; if (m) unread = parseInt(m[1], 10); } chats.push({ name, lastMessage, timestamp, unread }); } catch {} } res.json({ chats }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/chat/fetchMessages/:session', async (req, res) => { try { const { chatId } = req.body; if (!chatId) return res.status(400).json({ error: 'chatId required' }); const d = await getDriver(); await ensureWhatsApp(d); const query = chatId.replace(/@[cg]\.us$/, ''); await openChat(d, query); await d.pause(2000); const layoutEls = await d.$$(uia('resourceId("com.whatsapp:id/main_layout")')); const messages = []; for (const layout of layoutEls) { try { const textEl = await layout.$(uia('resourceId("com.whatsapp:id/message_text")')); const dateEl = await layout.$( uia('resourceId("com.whatsapp:id/date")') ); if (await textEl.isExisting()) { messages.push({ text: await textEl.getText(), timestamp: (await dateEl.isExisting()) ? await dateEl.getText() : '', }); } } catch {} } messages.reverse(); goHome(d).catch(() => {}); res.json({ messages }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/client/sendMessage/:session', async (req, res) => { try { const { chatId, content } = req.body; if (!chatId) return res.status(400).json({ error: 'chatId required' }); if (!content) return res.status(400).json({ error: 'content required' }); const d = await getDriver(); await ensureWhatsApp(d); const query = chatId.replace(/@[cg]\.us$/, ''); await openChat(d, query); const input = await find(d, uia('resourceId("com.whatsapp:id/entry")'), 5000); if (!input) throw new Error('Could not find message input'); await input.setValue(content); await d.pause(500); let sendBtn = await find(d, uia('resourceId("com.whatsapp:id/send")'), 3000); if (!sendBtn) { sendBtn = await find(d, uia('descriptionContains("Send")'), 2000); } if (!sendBtn) throw new Error('Could not find send button'); await sendBtn.click(); await d.pause(1000); goHome(d).catch(() => {}); res.json({ status: 'sent', content }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.listen(PORT, '0.0.0.0', () => { console.log(`wa-appium-api listening on http://0.0.0.0:${PORT}`); console.log(` Appium server: ${APPIUM_HOST}:${APPIUM_PORT}`); });