Compare commits
No commits in common. "new-pipeline" and "main" have entirely different histories.
new-pipeli
...
main
22
.env.example
22
.env.example
@ -14,7 +14,8 @@ TELEGRAM_CHAT_ID=your_chat_id
|
|||||||
BATCH_INTERVAL_MS=900000
|
BATCH_INTERVAL_MS=900000
|
||||||
BATCH_MAX_CHARS=700
|
BATCH_MAX_CHARS=700
|
||||||
INCLUDE_OWN_MESSAGES=true
|
INCLUDE_OWN_MESSAGES=true
|
||||||
OWN_NAME=yourname
|
OWN_NAME=Me
|
||||||
|
OWN_LAST_NAME=
|
||||||
|
|
||||||
# Keep-alive (ping a URL to prevent hosting sleep)
|
# Keep-alive (ping a URL to prevent hosting sleep)
|
||||||
KEEP_ALIVE_URL=
|
KEEP_ALIVE_URL=
|
||||||
@ -23,22 +24,3 @@ KEEP_ALIVE_INTERVAL_MS=300000
|
|||||||
# Google Apps Script Web App URL (for media uploads to Drive)
|
# 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
|
# Deploy assets/google-scripts/listener.js as a Web App and paste the URL here
|
||||||
APPS_SCRIPT_URL=
|
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=
|
|
||||||
13
.woodpecker.yaml
Normal file
13
.woodpecker.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
@ -1,19 +0,0 @@
|
|||||||
steps:
|
|
||||||
build-and-push:
|
|
||||||
name: Build & Push Image
|
|
||||||
image: harbor.dvirlabs.com/base-images/plugin-kaniko
|
|
||||||
when:
|
|
||||||
branch: [ master, develop, new-pipeline ]
|
|
||||||
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
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM harbor.dvirlabs.com/base-images/node:22-slim
|
FROM node:22-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
chromium \
|
chromium \
|
||||||
|
|||||||
@ -22,16 +22,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
includeOwnMessages: process.env.INCLUDE_OWN_MESSAGES !== 'false',
|
includeOwnMessages: process.env.INCLUDE_OWN_MESSAGES !== 'false',
|
||||||
ownName: process.env.OWN_NAME || 'Me',
|
ownName: [process.env.OWN_NAME, process.env.OWN_LAST_NAME].filter(Boolean).join(' ') || 'Me',
|
||||||
|
|
||||||
appsScriptUrl: process.env.APPS_SCRIPT_URL || '',
|
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: {
|
keepAlive: {
|
||||||
url: process.env.KEEP_ALIVE_URL || '',
|
url: process.env.KEEP_ALIVE_URL || '',
|
||||||
|
|||||||
136
index.js
136
index.js
@ -18,7 +18,6 @@ let msgCounter = 0;
|
|||||||
|
|
||||||
let startTime = Date.now();
|
let startTime = Date.now();
|
||||||
let totalForwarded = 0;
|
let totalForwarded = 0;
|
||||||
let smsReplies = 0;
|
|
||||||
let userStats = {};
|
let userStats = {};
|
||||||
let groupStats = {};
|
let groupStats = {};
|
||||||
|
|
||||||
@ -78,8 +77,6 @@ async function flushQueue() {
|
|||||||
if (restarting) return;
|
if (restarting) return;
|
||||||
if (messageQueue.length === 0) return;
|
if (messageQueue.length === 0) return;
|
||||||
|
|
||||||
log('FLUSH', `Flushing ${messageQueue.length} queued messages`);
|
|
||||||
|
|
||||||
flushTimer = null;
|
flushTimer = null;
|
||||||
flushTimerStart = null;
|
flushTimerStart = null;
|
||||||
|
|
||||||
@ -87,44 +84,32 @@ async function flushQueue() {
|
|||||||
messageQueue = [];
|
messageQueue = [];
|
||||||
msgCounter = 0;
|
msgCounter = 0;
|
||||||
|
|
||||||
const prodBatch = batch.filter((m) => !m.isTest);
|
const text = formatBatch(batch);
|
||||||
const testBatch = batch.filter((m) => m.isTest);
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
await sendSMS(text, recipient);
|
await sendSMS(text);
|
||||||
log('INFO', `Flushed ${subset.length} ${label} messages to ${recipient}`);
|
log('INFO', `Flushed ${batch.length} messages`);
|
||||||
for (const m of subset) {
|
for (const m of batch) {
|
||||||
userStats[m.sender] = (userStats[m.sender] || 0) + 1;
|
userStats[m.sender] = (userStats[m.sender] || 0) + 1;
|
||||||
groupStats[m.group] = (groupStats[m.group] || 0) + 1;
|
groupStats[m.group] = (groupStats[m.group] || 0) + 1;
|
||||||
totalForwarded++;
|
totalForwarded++;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('ERROR', `${label} flush failed for ${recipient}: ${err.message}`);
|
log('ERROR', `Flush failed: ${err.message}`);
|
||||||
messageQueue = subset.concat(messageQueue);
|
messageQueue = batch.concat(messageQueue);
|
||||||
msgCounter = messageQueue.length;
|
msgCounter = messageQueue.length;
|
||||||
|
scheduleFlush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doFlush(prodBatch, config.smsGateway.recipientNumber, 'prod');
|
function enqueue(group, sender, text, showSender = true) {
|
||||||
const testRecipient = config.test.smsRecipient || config.smsGateway.recipientNumber;
|
|
||||||
await doFlush(testBatch, testRecipient, 'test');
|
|
||||||
|
|
||||||
if (messageQueue.length > 0) scheduleFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
function enqueue(group, sender, text, showSender = true, isTest = false) {
|
|
||||||
msgCounter++;
|
msgCounter++;
|
||||||
messageQueue.push({ group, sender, text, showSender, isTest });
|
messageQueue.push({ group, sender, text, showSender });
|
||||||
|
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
const tag = isTest ? '[TEST]' : '[PROD]';
|
log('QUEUE', `Queue #${msgCounter} - Message from ${sender}, flushed at ${flushTime()}`);
|
||||||
log('QUEUE', `Queue #${msgCounter} ${tag} ${sender} → ${group}: ${text.length > 80 ? text.slice(0, 80) + '...' : text}`);
|
|
||||||
|
console.log(formatBatch(messageQueue))
|
||||||
|
|
||||||
if (queueSize() >= config.batch.maxChars) {
|
if (queueSize() >= config.batch.maxChars) {
|
||||||
clearTimeout(flushTimer);
|
clearTimeout(flushTimer);
|
||||||
@ -183,9 +168,7 @@ async function startClient() {
|
|||||||
log('INIT', 'Starting OmegaBaSMS...');
|
log('INIT', 'Starting OmegaBaSMS...');
|
||||||
await killClient();
|
await killClient();
|
||||||
|
|
||||||
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
|
log('INIT', `Groups to monitor: ${config.groupNames.join(', ')}`);
|
||||||
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', `Batch interval: ${config.batch.intervalMs / 1000}s / max ${config.batch.maxChars} chars`);
|
||||||
log('INIT', `Forwarding SMS to: ${config.smsGateway.recipientNumber}`);
|
log('INIT', `Forwarding SMS to: ${config.smsGateway.recipientNumber}`);
|
||||||
|
|
||||||
@ -224,9 +207,8 @@ async function startClient() {
|
|||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
restarting = false;
|
restarting = false;
|
||||||
restartDelay = 1000;
|
restartDelay = 1000;
|
||||||
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
|
|
||||||
log('READY', 'WhatsApp connected successfully');
|
log('READY', 'WhatsApp connected successfully');
|
||||||
log('READY', `Monitoring ${allGroups.length} group(s): ${allGroups.join(', ')}`);
|
log('READY', `Monitoring ${config.groupNames.length} group(s): ${config.groupNames.join(', ')}`);
|
||||||
log('READY', `Forwarding to ${config.smsGateway.recipientNumber}`);
|
log('READY', `Forwarding to ${config.smsGateway.recipientNumber}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -272,10 +254,8 @@ async function startClient() {
|
|||||||
|
|
||||||
const chat = await message.getChat();
|
const chat = await message.getChat();
|
||||||
if (!chat.isGroup) return;
|
if (!chat.isGroup) return;
|
||||||
const isTest = config.test.groupNames.length && config.test.groupNames.includes(chat.name);
|
if (!config.groupNames.includes(chat.name)) return;
|
||||||
if (!config.groupNames.includes(chat.name) && !isTest) return;
|
|
||||||
if (message.fromMe && !config.includeOwnMessages) 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 contact = await message.getContact();
|
||||||
const sender = message.fromMe
|
const sender = message.fromMe
|
||||||
@ -338,7 +318,7 @@ async function startClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue(chat.name, sender, body, !isReply && !message.isForwarded, isTest);
|
enqueue(chat.name, sender, body, !isReply && !message.isForwarded);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('ERROR', `Message handler: ${err.message}`);
|
log('ERROR', `Message handler: ${err.message}`);
|
||||||
}
|
}
|
||||||
@ -375,7 +355,7 @@ function renderDashboard(clientState) {
|
|||||||
|
|
||||||
const api = JSON.stringify({
|
const api = JSON.stringify({
|
||||||
uptime, uptimeStr, connected,
|
uptime, uptimeStr, connected,
|
||||||
totalForwarded, smsReplies, queued: messageQueue.length, flushTime: flushTime(),
|
totalForwarded, queued: messageQueue.length, flushTime: flushTime(),
|
||||||
userStats, groupStats,
|
userStats, groupStats,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -433,7 +413,6 @@ function renderDashboard(clientState) {
|
|||||||
<div class="card"><div class="label">Status</div><div class="value" id="status">—</div></div>
|
<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">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">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 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>
|
||||||
<div class="tables">
|
<div class="tables">
|
||||||
@ -449,7 +428,6 @@ async function poll() {
|
|||||||
document.getElementById('status').innerHTML = '<span class="status-dot ' + (d.connected ? 'on' : 'off') + '"></span>' + (d.connected ? 'Connected' : 'Disconnected');
|
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('uptime').textContent = d.uptimeStr;
|
||||||
document.getElementById('forwarded').textContent = d.totalForwarded;
|
document.getElementById('forwarded').textContent = d.totalForwarded;
|
||||||
document.getElementById('smsReplies').textContent = d.smsReplies;
|
|
||||||
document.getElementById('queued').textContent = d.queued;
|
document.getElementById('queued').textContent = d.queued;
|
||||||
document.getElementById('flushTime').textContent = d.queued > 0 ? 'flushed at ' + d.flushTime : '';
|
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>';
|
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>';
|
||||||
@ -496,7 +474,6 @@ const server = http.createServer(async (req, res) => {
|
|||||||
connected: clientState === 'CONNECTED',
|
connected: clientState === 'CONNECTED',
|
||||||
clientState,
|
clientState,
|
||||||
totalForwarded,
|
totalForwarded,
|
||||||
smsReplies,
|
|
||||||
queued: messageQueue.length,
|
queued: messageQueue.length,
|
||||||
flushTime: flushTime(),
|
flushTime: flushTime(),
|
||||||
userStats,
|
userStats,
|
||||||
@ -504,85 +481,6 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
return;
|
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) {
|
|
||||||
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.sender || '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;
|
|
||||||
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().startsWith(groupName.toLowerCase()));
|
|
||||||
if (!matchedGroup) {
|
|
||||||
log('SMS', `Group "${groupName}" not found in ${candidates.join(', ')}`);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const smsBody = `[${from}]\n ${replyText}`;
|
|
||||||
log('SMS', `Sending${isTest ? ' [TEST]' : ''} reply to WhatsApp group "${matchedGroup}": ${smsBody}`);
|
|
||||||
await chat.sendMessage(smsBody);
|
|
||||||
smsReplies++;
|
|
||||||
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();
|
const clientState = await getClientState();
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.end(renderDashboard(clientState));
|
res.end(renderDashboard(clientState));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user