Update .woodpecker.yml #1

Open
dvirlabs wants to merge 9 commits from new-pipeline into main
3 changed files with 142 additions and 24 deletions
Showing only changes of commit 6d440225ad - Show all commits

View File

@ -14,8 +14,7 @@ TELEGRAM_CHAT_ID=your_chat_id
BATCH_INTERVAL_MS=900000
BATCH_MAX_CHARS=700
INCLUDE_OWN_MESSAGES=true
OWN_NAME=Me
OWN_LAST_NAME=
OWN_NAME=yourname
# Keep-alive (ping a URL to prevent hosting sleep)
KEEP_ALIVE_URL=
@ -24,3 +23,22 @@ KEEP_ALIVE_INTERVAL_MS=300000
# Google Apps Script Web App URL (for media uploads to Drive)
# Deploy assets/google-scripts/listener.js as a Web App and paste the URL here
APPS_SCRIPT_URL=
# SMS reply webhook — set a token, configure TextBee to POST to
# https://<your-host>/webhook/sms?token=<this_token>
SMS_WEBHOOK_TOKEN=
# ── Test / dry-run values ──────────────────────────────────
# These let you test features side-by-side with production.
# Messages from TEST_GROUP_NAMES are forwarded to TEST_SMS_RECIPIENT.
# SMS replies from TEST_SMS_FROM are routed to TEST_GROUP_NAMES.
# Groups to monitor for testing (comma-separated)
TEST_GROUP_NAMES=
# SMS recipient for test forwards (defaults to SMS_RECIPIENT if empty)
TEST_SMS_RECIPIENT=
# Phone number of a test device; SMS replies from this number
# will be routed to test groups instead of production groups
TEST_SMS_FROM=

View File

@ -22,9 +22,16 @@ module.exports = {
},
includeOwnMessages: process.env.INCLUDE_OWN_MESSAGES !== 'false',
ownName: [process.env.OWN_NAME, process.env.OWN_LAST_NAME].filter(Boolean).join(' ') || 'Me',
ownName: process.env.OWN_NAME || 'Me',
appsScriptUrl: process.env.APPS_SCRIPT_URL || '',
smsWebhookToken: process.env.SMS_WEBHOOK_TOKEN || '',
test: {
groupNames: (process.env.TEST_GROUP_NAMES || '').split(',').map(s => s.trim()).filter(Boolean),
smsRecipient: process.env.TEST_SMS_RECIPIENT || '',
smsFrom: process.env.TEST_SMS_FROM || '',
},
keepAlive: {
url: process.env.KEEP_ALIVE_URL || '',

121
index.js
View File

@ -18,6 +18,7 @@ let msgCounter = 0;
let startTime = Date.now();
let totalForwarded = 0;
let smsReplies = 0;
let userStats = {};
let groupStats = {};
@ -84,27 +85,37 @@ async function flushQueue() {
messageQueue = [];
msgCounter = 0;
const text = formatBatch(batch);
const prodBatch = batch.filter((m) => !m.isTest);
const testBatch = batch.filter((m) => m.isTest);
async function doFlush(subset, recipient) {
if (subset.length === 0) return;
const text = formatBatch(subset);
try {
await sendSMS(text);
log('INFO', `Flushed ${batch.length} messages`);
for (const m of batch) {
await sendSMS(text, recipient);
log('INFO', `Flushed ${subset.length} messages to ${recipient}`);
for (const m of subset) {
userStats[m.sender] = (userStats[m.sender] || 0) + 1;
groupStats[m.group] = (groupStats[m.group] || 0) + 1;
totalForwarded++;
}
} catch (err) {
log('ERROR', `Flush failed: ${err.message}`);
messageQueue = batch.concat(messageQueue);
log('ERROR', `Flush failed for ${recipient}: ${err.message}`);
messageQueue = subset.concat(messageQueue);
msgCounter = messageQueue.length;
scheduleFlush();
}
}
await doFlush(prodBatch, config.smsGateway.recipientNumber);
const testRecipient = config.test.smsRecipient || config.smsGateway.recipientNumber;
await doFlush(testBatch, testRecipient);
if (messageQueue.length > 0) scheduleFlush();
}
function enqueue(group, sender, text, showSender = true) {
function enqueue(group, sender, text, showSender = true, isTest = false) {
msgCounter++;
messageQueue.push({ group, sender, text, showSender });
messageQueue.push({ group, sender, text, showSender, isTest });
scheduleFlush();
log('QUEUE', `Queue #${msgCounter} - Message from ${sender}, flushed at ${flushTime()}`);
@ -168,7 +179,9 @@ async function startClient() {
log('INIT', 'Starting OmegaBaSMS...');
await killClient();
log('INIT', `Groups to monitor: ${config.groupNames.join(', ')}`);
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
log('INIT', `Groups to monitor: ${allGroups.join(', ')}`);
if (config.test.groupNames.length) log('INIT', `Test groups: ${config.test.groupNames.join(', ')} → SMS to ${config.test.smsRecipient || config.smsGateway.recipientNumber}`);
log('INIT', `Batch interval: ${config.batch.intervalMs / 1000}s / max ${config.batch.maxChars} chars`);
log('INIT', `Forwarding SMS to: ${config.smsGateway.recipientNumber}`);
@ -207,8 +220,9 @@ async function startClient() {
client.on('ready', () => {
restarting = false;
restartDelay = 1000;
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
log('READY', 'WhatsApp connected successfully');
log('READY', `Monitoring ${config.groupNames.length} group(s): ${config.groupNames.join(', ')}`);
log('READY', `Monitoring ${allGroups.length} group(s): ${allGroups.join(', ')}`);
log('READY', `Forwarding to ${config.smsGateway.recipientNumber}`);
});
@ -254,7 +268,8 @@ async function startClient() {
const chat = await message.getChat();
if (!chat.isGroup) return;
if (!config.groupNames.includes(chat.name)) return;
const isTest = config.test.groupNames.length && config.test.groupNames.includes(chat.name);
if (!config.groupNames.includes(chat.name) && !isTest) return;
if (message.fromMe && !config.includeOwnMessages) return;
const contact = await message.getContact();
@ -318,7 +333,7 @@ async function startClient() {
}
}
enqueue(chat.name, sender, body, !isReply && !message.isForwarded);
enqueue(chat.name, sender, body, !isReply && !message.isForwarded, isTest);
} catch (err) {
log('ERROR', `Message handler: ${err.message}`);
}
@ -355,7 +370,7 @@ function renderDashboard(clientState) {
const api = JSON.stringify({
uptime, uptimeStr, connected,
totalForwarded, queued: messageQueue.length, flushTime: flushTime(),
totalForwarded, smsReplies, queued: messageQueue.length, flushTime: flushTime(),
userStats, groupStats,
});
@ -413,6 +428,7 @@ function renderDashboard(clientState) {
<div class="card"><div class="label">Status</div><div class="value" id="status"></div></div>
<div class="card"><div class="label">Uptime</div><div class="value blue" id="uptime"></div></div>
<div class="card"><div class="label">Forwarded</div><div class="value green" id="forwarded"></div></div>
<div class="card"><div class="label">SMS Replies</div><div class="value blue" id="smsReplies"></div></div>
<div class="card"><div class="label">Queued</div><div class="value yellow" id="queued"></div><div style="font-size:0.75rem;color:#64748b;margin-top:0.25rem" id="flushTime"></div></div>
</div>
<div class="tables">
@ -428,6 +444,7 @@ async function poll() {
document.getElementById('status').innerHTML = '<span class="status-dot ' + (d.connected ? 'on' : 'off') + '"></span>' + (d.connected ? 'Connected' : 'Disconnected');
document.getElementById('uptime').textContent = d.uptimeStr;
document.getElementById('forwarded').textContent = d.totalForwarded;
document.getElementById('smsReplies').textContent = d.smsReplies;
document.getElementById('queued').textContent = d.queued;
document.getElementById('flushTime').textContent = d.queued > 0 ? 'flushed at ' + d.flushTime : '';
document.getElementById('users').innerHTML = Object.entries(d.userStats).sort((a,b) => b[1]-a[1]).map(([n,c]) => '<tr><td>' + n + '</td><td>' + c + '</td></tr>').join('') || '<tr><td colspan="2" style="color:#64748b;">No messages yet</td></tr>';
@ -474,6 +491,7 @@ const server = http.createServer(async (req, res) => {
connected: clientState === 'CONNECTED',
clientState,
totalForwarded,
smsReplies,
queued: messageQueue.length,
flushTime: flushTime(),
userStats,
@ -481,6 +499,81 @@ const server = http.createServer(async (req, res) => {
}));
return;
}
if (req.url.startsWith('/api/webhook/sms') && req.method === 'POST') {
const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const token = urlObj.searchParams.get('token');
if (config.smsWebhookToken && token !== config.smsWebhookToken) {
res.writeHead(403);
res.end('Forbidden');
return;
}
let raw = '';
req.on('data', (chunk) => (raw += chunk));
await new Promise((r) => req.on('end', r));
let data;
try {
data = JSON.parse(raw);
} catch {
res.writeHead(400);
res.end('Invalid JSON');
return;
}
const from = data.from || 'Unknown';
const msg = (data.message || '').trim();
if (!msg) {
res.writeHead(400);
res.end('Empty message');
return;
}
const colonIdx = msg.indexOf(':');
if (colonIdx === -1) {
res.writeHead(400);
res.end('Format: <group name>: <message>');
return;
}
const groupName = msg.slice(0, colonIdx).trim();
const replyText = msg.slice(colonIdx + 1).trim();
if (!replyText) {
res.writeHead(400);
res.end('Empty reply');
return;
}
const isTest = config.test.smsFrom && from === config.test.smsFrom;
const candidates = isTest && config.test.groupNames.length ? config.test.groupNames : config.groupNames;
const matchedGroup = candidates.find((g) => g.toLowerCase() === groupName.toLowerCase());
if (!matchedGroup) {
res.writeHead(404);
res.end(`Group "${groupName}" not found`);
return;
}
try {
const chats = await client.getChats();
const chat = chats.find((c) => c.isGroup && c.name === matchedGroup);
if (!chat) {
res.writeHead(404);
res.end(`Chat "${matchedGroup}" not found on WhatsApp`);
return;
}
await chat.sendMessage(`[${from}]\n ${replyText}`);
smsReplies++;
log('SMS', `Reply sent to "${matchedGroup}" from ${from}: ${replyText}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, group: matchedGroup }));
} catch (err) {
log('ERROR', `SMS reply failed: ${err.message}`);
res.writeHead(500);
res.end(`Failed: ${err.message}`);
}
return;
}
const clientState = await getClientState();
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(renderDashboard(clientState));