Initial commit
This commit is contained in:
commit
ccd9f0ddd0
1 changed files with 995 additions and 0 deletions
251
wa-appium-api/server.js
Normal file
251
wa-appium-api/server.js
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
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}`);
|
||||
});
|
||||
Loading…
Reference in a new issue