Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data/stores/*
data/sessions/*
.env
.yarn/*
.yarnrc.yml
.yarnrc.yml
/.idea/
154 changes: 131 additions & 23 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,16 @@ <h5 class="modal-title" id="editModalLabel">Editar/Adicionar Sessão</h5>
</div>
<div class="modal-body">
<form id="editForm">
<div class="row edit-form-row">
<div class="col-12 col-md-12 col-lg-6">
<label for="provider" class="form-label">Provider</label>
<select id="provider" name="provider" class="form-control">
<option value="baileys">baileys</option>
<option value="whatsmeow">whatsmeow</option>
<option value="forwarder">forwarder</option>
</select>
</div>
</div>
<div class="row edit-form-row">
<div class="col-12 col-md-12 col-lg-6">
<label for="label" class="form-label">Identificação</label>
Expand Down Expand Up @@ -510,6 +520,8 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>
let currentPhoneNumber = ''
let currentQrTimeOut = ''
let lastTimeout = undefined
let qrPollIntervalId = undefined
let statusPollIntervalId = undefined

const onBroadcast = function(data) {
if (data.phone == currentPhoneNumber) {
Expand Down Expand Up @@ -712,24 +724,35 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>
table.clear();

sessions.forEach(session => {
const statusLower = (session.status || '').toLowerCase()
const disconnectBtn = ['online', 'connecting'].includes(statusLower)
? `<button class="btn btn-sm btn-secondary disconnect-btn" data-number="${session.display_phone_number}">
<i class="fas fa-unlink"></i> Desconectar
</button>`
: '';
const connectBtn = !['online', 'connecting'].includes(statusLower)
? `<button class=\"btn btn-sm btn-success connect-btn\" data-bs-toggle=\"modal\" data-bs-target=\"#connectModal\" data-number=\"${session.display_phone_number}\">
<i class=\"fas fa-link\"></i> Conectar
</button>`
: '';
const buttons = `
<button class=\"btn btn-sm btn-warning edit-btn\" data-bs-toggle=\"modal\" data-bs-target=\"#editModal\" data-number=\"${session.display_phone_number}\">
<i class=\"fas fa-edit\"></i> Editar
</button>
${connectBtn}
${disconnectBtn}
<button class=\"btn btn-sm btn-danger delete-btn\" data-number=\"${session.display_phone_number}\">
<i class=\"fas fa-trash\"></i> Deletar
</button>
<button class=\"btn btn-sm btn-success message-btn\" data-bs-target=\"#messageModal\" data-number=\"${session.display_phone_number}\">
<i class=\"fas fa-message\"></i> Testar
</button>`;
table.row.add([
session.label,
session.display_phone_number,
session.status,
session.server,
`
<button class="btn btn-sm btn-warning edit-btn" data-bs-toggle="modal" data-bs-target="#editModal" data-number="${session.display_phone_number}">
<i class="fas fa-edit"></i> Editar
</button>
<button class="btn btn-sm btn-success connect-btn" data-bs-toggle="modal" data-bs-target="#connectModal" data-number="${session.display_phone_number}">
<i class="fas fa-link"></i> Conectar
</button>
<button class="btn btn-sm btn-danger delete-btn" data-number="${session.display_phone_number}">
<i class="fas fa-trash"></i> Deletar
</button>
<button class="btn btn-sm btn-success message-btn" data-bs-target="#messageModal" data-number="${session.display_phone_number}">
<i class="fas fa-message"></i> Testar
</button>`
buttons
]).draw(false);
});
}
Expand Down Expand Up @@ -760,6 +783,27 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>

});

//Disconnect button
$('#sessionsTable tbody').on('click', '.disconnect-btn', function() {
const number = $(this).data('number');
const token = localStorage.getItem(tokenKey);
if (!token) { alert('Token não encontrado.'); return }
fetch(`${apiUrl}/sessions/${number}/disconnect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) throw new Error('Erro ao desconectar sessão.');
alert('Sessão desconectada.');
// opcional: recarrega dados
fetchSessions(token, false);
})
.catch(error => alert(error.message));
});

//Edit button
$('#sessionsTable tbody').on('click', '.edit-btn', function () {
const number = $(this).data('number');
Expand Down Expand Up @@ -793,6 +837,7 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>

//Edit form for session number
function populateEditForm(session) {
$('#provider').val(session.provider || 'baileys');
$('#label').val(session.label);
$('#authToken').val(session.authToken);
$('#wavoipToken').val(session.wavoipToken);
Expand Down Expand Up @@ -886,6 +931,7 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>

if (token && number) {
const sessionData = {
provider: $('#provider').val(),
label: $('#label').val(),
authToken: $('#authToken').val(),
wavoipToken: $('#wavoipToken').val(),
Expand Down Expand Up @@ -969,15 +1015,20 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>
return response.json();
})
.then(data => {
if (data.status === 'offline' || data.status === 'disconnected') {
attemptRegister(number, token);
} else if (data.status === 'connecting') {
connectToWebSocket(number, data.qrTimeoutMs);
} else if (data.status === 'online') {
document.getElementById('qrCodeContainer').innerHTML = '';
document.getElementById('qrCodeMessage').textContent = 'Sessão já está Conectada';
const provider = data.provider || 'baileys';
if (provider === 'whatsmeow') {
startWhatsmeowConnectFlow(number, token);
} else {
console.log("Status não está offline nem connecting.");
if (data.status === 'offline' || data.status === 'disconnected') {
attemptRegister(number, token);
} else if (data.status === 'connecting') {
connectToWebSocket(number, data.qrTimeoutMs);
} else if (data.status === 'online') {
document.getElementById('qrCodeContainer').innerHTML = '';
document.getElementById('qrCodeMessage').textContent = 'Sessão já está Conectada';
} else {
console.log("Status não está offline nem connecting.");
}
}
})
.catch(error => {
Expand Down Expand Up @@ -1062,10 +1113,67 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>
}
}

// Whatsmeow connect flow
function startWhatsmeowConnectFlow(number, token) {
// clear old timers
if (qrPollIntervalId) { clearInterval(qrPollIntervalId); qrPollIntervalId = undefined }
if (statusPollIntervalId) { clearInterval(statusPollIntervalId); statusPollIntervalId = undefined }

currentPhoneNumber = number
document.getElementById('qrCodeMessage').textContent = 'Preparando conexão (whatsmeow)...';
document.getElementById('qrCodeContainer').innerHTML = '';

// trigger adapter connect via UNO
fetch(`${apiUrl}/sessions/${number}/connect`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
}).catch(_ => {})

// poll QR image
qrPollIntervalId = setInterval(() => {
fetch(`${apiUrl}/sessions/${number}/qr`, { headers: { Authorization: `Bearer ${token}` } })
.then(resp => {
if (resp.ok) {
return resp.text()
}
throw new Error('no qr')
})
.then(base64 => {
const img = `data:image/png;base64,${base64}`
const qrcodeDiv = document.getElementById('qrCodeContainer');
qrcodeDiv.innerHTML = `<img src="${img}" alt="QR Code">`;
document.getElementById('qrCodeMessage').textContent = 'Leia o QR Code Abaixo';
})
.catch(_ => {})
}, 1500)

// poll status
statusPollIntervalId = setInterval(() => {
fetch(`${apiUrl}/v15.0/${number}`, { headers: { Authorization: `Bearer ${token}` } })
.then(r => r.json())
.then(data => {
const statusDiv = document.getElementById('qrCodeMessage');
if (data.status === 'online') {
statusDiv.textContent = 'Conectado!';
if (qrPollIntervalId) { clearInterval(qrPollIntervalId); qrPollIntervalId = undefined }
if (statusPollIntervalId) { clearInterval(statusPollIntervalId); statusPollIntervalId = undefined }
document.getElementById('qrCodeContainer').innerHTML = '';
} else if (data.status === 'connecting') {
statusDiv.textContent = 'Aguardando leitura do QR Code...';
} else if (data.status === 'disconnected' || data.status === 'offline') {
statusDiv.textContent = `Status: ${data.status}`;
}
})
.catch(_ => {})
}, 2000)
}

// disconnect websocket on modal close
$('#connectModal').on('hidden.bs.modal', function () {
currentPhoneNumber = ''
currentQrTimeOut = ''
if (qrPollIntervalId) { clearInterval(qrPollIntervalId); qrPollIntervalId = undefined }
if (statusPollIntervalId) { clearInterval(statusPollIntervalId); statusPollIntervalId = undefined }
document.getElementById('qrCodeContainer').innerHTML = '';
document.getElementById('qrCodeMessage').textContent = 'Aguarde o QrCode/PairingCode';
});
Expand Down Expand Up @@ -1119,5 +1227,5 @@ <h5 class="modal-title" id="messageModalLabel">Testar Sessão</h5>
}
}
</script>
</body>
</html>
</body>
</html>
6 changes: 6 additions & 0 deletions src/amqp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const amqpGetChannel = async () => {
logger.info('Creating channel...')
await amqpConnect()
amqpChannel = await amqpChannelModel?.createChannel()
amqpChannel?.setMaxListeners(0)
logger.info('Created channel!')
}
return amqpChannel
Expand Down Expand Up @@ -340,6 +341,11 @@ export const amqpConsume = async (

const bindingKey = await bindQueue(channel, exchange, queue, routingKey)
const bindingKeyDelayed = await bindQueue(channel, exchange, queue, routingKey, true)
// For bridge (direct) exchange, also bind a plain routing key without queue prefix
// so external adapters can publish using only the phone number as routing key.
if (exchange === UNOAPI_EXCHANGE_BRIDGE_NAME && routingKey) {
await channel?.bindQueue(queue, exchange, routingKey)
}

channel?.on('close', () => {
channel.unbindQueue(queue, exchange, bindingKey)
Expand Down
43 changes: 35 additions & 8 deletions src/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
UNOAPI_QUEUE_TRANSCRIBER,
} from './defaults'

import { amqpConsume } from './amqp'
import { amqpConsume, amqpGetChannel, extractRoutingKeyFromBindingKey } from './amqp'
import { startRedis } from './services/redis'
import { OutgoingCloudApi } from './services/outgoing_cloud_api'
import { getConfigRedis } from './services/config_redis'
Expand All @@ -37,6 +37,11 @@ import { addToBlacklist } from './jobs/add_to_blacklist'
import { TimerJob } from './jobs/timer'
import { TranscriberJob } from './jobs/transcriber'
import { OutgoingAmqp } from './services/outgoing_amqp'
import { IncomingBaileys } from './services/incoming_baileys'
import { getClientBaileys } from './services/client_baileys'
import { onNewLoginGenerateToken } from './services/on_new_login_generate_token'
import { ListenerAmqp } from './services/listener_amqp'
import { IncomingJob } from './jobs/incoming'

const incomingAmqp: Incoming = new IncomingAmqp(getConfigRedis)
const outgoingCloudApi: Outgoing = new OutgoingCloudApi(getConfigRedis, isInBlacklistInRedis, addToBlacklistRedis)
Expand Down Expand Up @@ -137,13 +142,35 @@ const startBroker = async () => {
}

logger.info('Starting blacklist add consumer %s', UNOAPI_SERVER_NAME)
await amqpConsume(
UNOAPI_EXCHANGE_BROKER_NAME,
UNOAPI_QUEUE_BLACKLIST_ADD,
'*',
addToBlacklist,
{ notifyFailedMessages, prefetch, type: 'topic' }
)
await amqpConsume(UNOAPI_EXCHANGE_BROKER_NAME, UNOAPI_QUEUE_BLACKLIST_ADD, '*', addToBlacklist, { notifyFailedMessages, prefetch, type: 'topic' })

// Consume provider-specific outgoing messages for Baileys sessions
const channel = await amqpGetChannel()
await channel?.assertExchange('unoapi.outgoing', 'topic', { durable: true })
await channel?.assertQueue('outgoing.baileys', { durable: true })
await channel?.bindQueue('outgoing.baileys', 'unoapi.outgoing', 'provider.baileys.*')
// Ensure Whatsmeow queues exist too (created but not consumed here)
await channel?.assertQueue('outgoing.baileys.dlq', { durable: true })
await channel?.assertQueue('outgoing.whatsmeow', { durable: true })
await channel?.bindQueue('outgoing.whatsmeow', 'unoapi.outgoing', 'provider.whatsmeow.*')
await channel?.assertQueue('outgoing.whatsmeow.dlq', { durable: true })
const listenerAmqpWorker = new ListenerAmqp()
const onNewLogin = onNewLoginGenerateToken(outgoingCloudApi)
const incomingBaileysWorker = new IncomingBaileys(listenerAmqpWorker, getConfigRedis, getClientBaileys, onNewLogin)
const providerJob = new IncomingJob(incomingBaileysWorker, outgoingAmqp, getConfigRedis)
channel?.consume('outgoing.baileys', async (payload) => {
if (!payload) {
return
}
const phone = extractRoutingKeyFromBindingKey(payload.fields.routingKey)
const data = JSON.parse(payload.content.toString())
try {
await providerJob.consume(phone, data)
} catch (error) {
logger.error(error, 'Error consuming provider.baileys message')
}
channel.ack(payload)
})

logger.info('Unoapi Cloud version %s started broker!', version)
}
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/phone_number_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class PhoneNumberController {
try {
const { phone } = req.params
const config = await this.getConfig(phone)
config.provider = config.provider || 'baileys'
const store = await config.getStore(phone, config)
logger.debug('Session store retrieved!')
const { sessionStore } = store
Expand Down Expand Up @@ -53,6 +54,7 @@ export class PhoneNumberController {
for (let i = 0, j = phones.length; i < j; i++) {
const phone = phones[i]
const config = await this.getConfig(phone)
config.provider = config.provider || 'baileys'
const store = await config.getStore(phone, config)
const { sessionStore } = store
const status = config.provider == 'forwarder' ? 'forwarder' : await sessionStore.getStatus(phone)
Expand Down
Loading