From c90405e6225ca4e575ddd29ec3ab040f4f3d3167 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Tue, 2 Jun 2026 21:04:51 +0300 Subject: [PATCH 1/9] Update .woodpecker.yml --- .woodpecker.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..f30cdf9 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,19 @@ +steps: + build-and-push: + name: Build & Push Image + image: harbor.dvirlabs.com/base-images/plugin-kaniko + when: + branch: [ master, develop ] + event: [ push, pull_request, tag ] + settings: + registry: harbor.dvirlabs.com + repo: my-apps/${CI_REPO_NAME} + dockerfile: Dockerfile + context: . + tags: + - latest + - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD -- 2.47.2 From d9e06c9127eb2448e1af29edf17b0de6c702c3a2 Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:43:20 +0300 Subject: [PATCH 2/9] Remove .woodpecker.yaml --- .woodpecker.yaml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .woodpecker.yaml diff --git a/.woodpecker.yaml b/.woodpecker.yaml deleted file mode 100644 index 8dab880..0000000 --- a/.woodpecker.yaml +++ /dev/null @@ -1,13 +0,0 @@ -steps: - build-and-push: - when: - event: [push] - branch: [main] - image: plugins/docker - settings: - repo: elisha852/omegabasms - tags: latest - username: - from_secret: DOCKER_USERNAME - password: - from_secret: DOCKER_PASSWORD \ No newline at end of file -- 2.47.2 From dbbaeacda49ad9c6f19c5b489f719c7a9a5bdf98 Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:45:03 +0300 Subject: [PATCH 3/9] test --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d96ef4a..b2fbdb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,4 +34,4 @@ COPY . . ENV PORT=3000 EXPOSE 3000 -CMD ["node", "index.js"] +CMD ["node", "index.js"] \ No newline at end of file -- 2.47.2 From 0e7c87479b4d465cb0659760f8282cbcd9975e7e Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:47:35 +0300 Subject: [PATCH 4/9] test --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index f30cdf9..208c003 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -3,7 +3,7 @@ steps: name: Build & Push Image image: harbor.dvirlabs.com/base-images/plugin-kaniko when: - branch: [ master, develop ] + branch: [ master, develop, new-pipeline ] event: [ push, pull_request, tag ] settings: registry: harbor.dvirlabs.com -- 2.47.2 From 43aa5966ce636914a20755467258ff7fd6ded83a Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:52:56 +0300 Subject: [PATCH 5/9] test --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 208c003..bc8a3f0 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -7,7 +7,7 @@ steps: event: [ push, pull_request, tag ] settings: registry: harbor.dvirlabs.com - repo: my-apps/${CI_REPO_NAME} + repo: my-apps/${CI_REPO_NAME,,} dockerfile: Dockerfile context: . tags: -- 2.47.2 From 2cb260a2483602d908c176ed226de7f8f3612f3e Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:59:11 +0300 Subject: [PATCH 6/9] Build and push app --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b2fbdb9..6d2434f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-slim +FROM harbor.dvirlabs.com/base-images/node:22-slim RUN apt-get update && apt-get install -y \ chromium \ -- 2.47.2 From 6d440225ad8a46ae229a274eb459506c61572ea1 Mon Sep 17 00:00:00 2001 From: elishadavidi <62501723+elishadavidi@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:39:16 +0300 Subject: [PATCH 7/9] Add support for returning messages --- .env.example | 24 ++++++++-- config.js | 9 +++- index.js | 133 +++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 142 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index c6431a4..610553f 100644 --- a/.env.example +++ b/.env.example @@ -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= @@ -23,4 +22,23 @@ 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= \ No newline at end of file +APPS_SCRIPT_URL= + +# SMS reply webhook — set a token, configure TextBee to POST to +# https:///webhook/sms?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= \ No newline at end of file diff --git a/config.js b/config.js index 6b1903c..0c593cb 100644 --- a/config.js +++ b/config.js @@ -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 || '', diff --git a/index.js b/index.js index 3df4de0..594441a 100644 --- a/index.js +++ b/index.js @@ -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); - try { - await sendSMS(text); - log('INFO', `Flushed ${batch.length} messages`); - for (const m of batch) { - userStats[m.sender] = (userStats[m.sender] || 0) + 1; - groupStats[m.group] = (groupStats[m.group] || 0) + 1; - totalForwarded++; + async function doFlush(subset, recipient) { + if (subset.length === 0) return; + const text = formatBatch(subset); + try { + 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 for ${recipient}: ${err.message}`); + messageQueue = subset.concat(messageQueue); + msgCounter = messageQueue.length; } - } catch (err) { - log('ERROR', `Flush failed: ${err.message}`); - messageQueue = batch.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) {
Status
Uptime
Forwarded
+
SMS Replies
Queued
@@ -428,6 +444,7 @@ async function poll() { document.getElementById('status').innerHTML = '' + (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]) => '' + n + '' + c + '').join('') || 'No messages yet'; @@ -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: : '); + 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)); -- 2.47.2 From 66ab869f29cf3c5be5500e2f36083982f634d5f7 Mon Sep 17 00:00:00 2001 From: elishadavidi <62501723+elishadavidi@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:20:57 +0300 Subject: [PATCH 8/9] Add debug logs --- index.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 594441a..a7b2f83 100644 --- a/index.js +++ b/index.js @@ -78,6 +78,8 @@ async function flushQueue() { if (restarting) return; if (messageQueue.length === 0) return; + log('FLUSH', `Flushing ${messageQueue.length} queued messages`); + flushTimer = null; flushTimerStart = null; @@ -88,27 +90,30 @@ async function flushQueue() { const prodBatch = batch.filter((m) => !m.isTest); const testBatch = batch.filter((m) => m.isTest); - async function doFlush(subset, recipient) { + if (testBatch.length) log('FLUSH', `Split: ${prodBatch.length} prod → ${config.smsGateway.recipientNumber}, ${testBatch.length} test → ${config.test.smsRecipient || config.smsGateway.recipientNumber}`); + + async function doFlush(subset, recipient, label) { if (subset.length === 0) return; const text = formatBatch(subset); + log('FLUSH', `Sending ${label} batch (${subset.length} msgs) to ${recipient}`); try { await sendSMS(text, recipient); - log('INFO', `Flushed ${subset.length} messages to ${recipient}`); + log('INFO', `Flushed ${subset.length} ${label} 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 for ${recipient}: ${err.message}`); + log('ERROR', `${label} flush failed for ${recipient}: ${err.message}`); messageQueue = subset.concat(messageQueue); msgCounter = messageQueue.length; } } - await doFlush(prodBatch, config.smsGateway.recipientNumber); + await doFlush(prodBatch, config.smsGateway.recipientNumber, 'prod'); const testRecipient = config.test.smsRecipient || config.smsGateway.recipientNumber; - await doFlush(testBatch, testRecipient); + await doFlush(testBatch, testRecipient, 'test'); if (messageQueue.length > 0) scheduleFlush(); } @@ -118,9 +123,8 @@ function enqueue(group, sender, text, showSender = true, isTest = false) { messageQueue.push({ group, sender, text, showSender, isTest }); scheduleFlush(); - log('QUEUE', `Queue #${msgCounter} - Message from ${sender}, flushed at ${flushTime()}`); - - console.log(formatBatch(messageQueue)) + const tag = isTest ? '[TEST]' : '[PROD]'; + log('QUEUE', `Queue #${msgCounter} ${tag} ${sender} → ${group}: ${text.length > 80 ? text.slice(0, 80) + '...' : text}`); if (queueSize() >= config.batch.maxChars) { clearTimeout(flushTimer); @@ -271,6 +275,7 @@ async function startClient() { 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; + if (isTest) log('TEST', `Message from test group "${chat.name}" by ${message.fromMe ? config.ownName : '?'}`); const contact = await message.getContact(); const sender = message.fromMe @@ -545,9 +550,11 @@ const server = http.createServer(async (req, res) => { } const isTest = config.test.smsFrom && from === config.test.smsFrom; + log('SMS', `Incoming webhook from ${from}${isTest ? ' (test mode)' : ''}: "${msg}"`); const candidates = isTest && config.test.groupNames.length ? config.test.groupNames : config.groupNames; const matchedGroup = candidates.find((g) => g.toLowerCase() === groupName.toLowerCase()); if (!matchedGroup) { + log('SMS', `Group "${groupName}" not found in ${candidates.join(', ')}`); res.writeHead(404); res.end(`Group "${groupName}" not found`); return; @@ -561,9 +568,10 @@ const server = http.createServer(async (req, res) => { res.end(`Chat "${matchedGroup}" not found on WhatsApp`); return; } - await chat.sendMessage(`[${from}]\n ${replyText}`); + const smsBody = `[${from}]\n ${replyText}`; + log('SMS', `Sending${isTest ? ' [TEST]' : ''} reply to WhatsApp group "${matchedGroup}": ${smsBody}`); + await chat.sendMessage(smsBody); 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) { -- 2.47.2 From 1e2527b5b58551cd9537b10b8284b62835ea68e6 Mon Sep 17 00:00:00 2001 From: elishadavidi <62501723+elishadavidi@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:26:47 +0300 Subject: [PATCH 9/9] Update sender --- index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index a7b2f83..9310374 100644 --- a/index.js +++ b/index.js @@ -505,6 +505,7 @@ const server = http.createServer(async (req, res) => { return; } if (req.url.startsWith('/api/webhook/sms') && req.method === 'POST') { + log('SMS', `Incoming sms webhook`); const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const token = urlObj.searchParams.get('token'); if (config.smsWebhookToken && token !== config.smsWebhookToken) { @@ -526,7 +527,7 @@ const server = http.createServer(async (req, res) => { return; } - const from = data.from || 'Unknown'; + const from = data.sender || 'Unknown'; const msg = (data.message || '').trim(); if (!msg) { res.writeHead(400); @@ -552,7 +553,7 @@ const server = http.createServer(async (req, res) => { const isTest = config.test.smsFrom && from === config.test.smsFrom; log('SMS', `Incoming webhook from ${from}${isTest ? ' (test mode)' : ''}: "${msg}"`); const candidates = isTest && config.test.groupNames.length ? config.test.groupNames : config.groupNames; - const matchedGroup = candidates.find((g) => g.toLowerCase() === groupName.toLowerCase()); + const matchedGroup = candidates.find((g) => g.toLowerCase().startsWith(groupName.toLowerCase())); if (!matchedGroup) { log('SMS', `Group "${groupName}" not found in ${candidates.join(', ')}`); res.writeHead(404); -- 2.47.2