Chat interna
La chat interna permette ai colleghi della stessa azienda di scriversi in tempo reale dal pannello web e dall'app ExoVoIP: conversazioni dirette e di gruppo, stato in linea, avviso «sta scrivendo», conferme di lettura e invio di file. È inclusa nel centralino e non richiede installazioni aggiuntive.
I messaggi sono protetti con cifratura end-to-end: solo tu e i destinatari potete leggerli. Né l'azienda né Exobyte possono vedere testi, nomi dei file o contenuti.
Chi può usarla
La chat è legata all'interno, non all'account: ogni persona scrive e riceve come il proprio interno.
- Pannello web: serve un utente con un interno associato. Gli utenti senza interno (ad esempio un account solo amministrativo) non vedono la chat.
- App ExoVoIP: ogni dispositivo collegato a un interno può usare la chat con quell'interno, su smartphone, tablet e computer.
Si scrive solo ai colleghi della propria azienda: la chat non attraversa mai aziende diverse. L'elenco dei contatti è quello degli interni dell'azienda, con nome, numero e stato in linea.
Conversazioni dirette e gruppi
Conversazione diretta: scegli un collega dall'elenco degli interni e scrivi. Tra due interni esiste una sola conversazione diretta: se è già stata aperta, si riprende da dove era rimasta.
Gruppo: dai un nome (fino a 80 caratteri) e scegli i partecipanti, fino a 200 per gruppo. Chi crea il gruppo ne è amministratore: può rinominarlo e aggiungere o togliere partecipanti. Ogni partecipante può uscire dal gruppo in qualsiasi momento. Le modifiche (nuovi membri, uscite, cambio di nome) compaiono nella conversazione come messaggi di sistema.
Ogni messaggio può contenere fino a 4.000 caratteri. Chi ha scritto un messaggio può eliminarlo: sparisce per tutti, insieme all'eventuale file allegato.
Una conversazione diretta si può nascondere dall'elenco; ricompare con il messaggio successivo.
Stato in linea
Accanto a ogni collega un indicatore colorato mostra se è raggiungibile. Lo stato unisce la chat e il telefono:
| Stato | Significato |
|---|---|
| In linea | Pannello o app aperti e in uso |
| Assente | Pannello o app aperti ma inattivi da qualche minuto |
| Al telefono | Il collega è impegnato in una chiamata |
| Non disturbare | Sull'interno è attivo il non disturbare |
| Non in linea | Nessun pannello o app aperti in questo momento |
Un collega non in linea riceve comunque i messaggi: li trova alla prossima apertura e, se ha l'app su smartphone o tablet, riceve subito una notifica.
Durante la scrittura gli altri partecipanti vedono l'avviso «sta scrivendo». Le conferme di lettura mostrano fino a quale messaggio ogni partecipante ha letto: nelle conversazioni dirette compare «Letto», nei gruppi il numero di persone che hanno letto.
File
Nelle conversazioni si possono inviare documenti, immagini e altri file, con un breve testo di accompagnamento facoltativo. Anche i file sono cifrati end-to-end: vengono cifrati sul dispositivo prima dell'invio e il centralino conserva solo il contenuto cifrato, senza nome né tipo.
| Regola | Valore |
|---|---|
| Dimensione massima | 20 MB per file |
| Disponibilità | 48 ore dall'invio, poi il file viene cancellato definitivamente |
| Anteprima | Immagini PNG, JPEG, GIF e WebP, generata sul dispositivo di chi riceve |
| File non ammessi | Eseguibili e script (ad esempio .exe, .msi, .bat, .js, .ps1, .sh, .apk, .dmg), bloccati dal pannello e dall'app prima dell'invio |
Dopo 48 ore il messaggio resta nella conversazione con l'indicazione File scaduto: il contenuto non è più recuperabile. Salva sul tuo dispositivo i file che ti servono più a lungo.
Sicurezza dei download
I file non hanno indirizzi pubblici. Si scaricano solo da una sessione autenticata del pannello o da un dispositivo collegato, e solo se si partecipa alla conversazione. Quando il browser o il telefono devono aprire il file con un collegamento (anteprima di un'immagine, download, condivisione), il centralino genera un link monouso valido 60 secondi, utilizzabile solo da chi lo ha richiesto. Un link copiato e inoltrato a un'altra persona non funziona.
Il file scaricato è cifrato: il pannello o l'app lo decifrano sul dispositivo, ne verificano l'integrità e solo allora lo mostrano o lo salvano con il nome originale.
L'amministratore può disattivare l'invio di file per tutta l'azienda (vedi Impostazioni dell'azienda).
Notifiche e popup
Pannello web: all'arrivo di un messaggio si apre in basso a destra un piccolo riquadro della conversazione, in stile messenger, con un avviso sonoro. Si può rispondere direttamente dal riquadro, ridurlo o aprire la conversazione completa. Il contatore dei messaggi non letti resta visibile nel menu.
App ExoVoIP:
- con l'app aperta, il messaggio compare in un popup in primo piano con l'avviso sonoro;
- con l'app chiusa o in background, smartphone e tablet ricevono una notifica con il nome del mittente («Nuovo messaggio da Mario Rossi», oppure «3 nuovi messaggi»); il testo non passa mai dalla notifica, che il centralino non può leggere. Su Android l'app, quando può, mostra l'anteprima decifrandola sul telefono; toccando la notifica si apre la conversazione;
- sul computer l'app aperta, anche ridotta a icona, mostra il popup e la notifica di sistema.
Le notifiche della chat sono distinte da quelle delle chiamate: per riceverle le notifiche di ExoVoIP devono essere consentite nelle impostazioni del telefono.
Non compaiono popup per i propri messaggi né per la conversazione già aperta sullo schermo: in quel caso i messaggi vengono segnati come letti.
Silenziare una conversazione
Ogni partecipante può silenziare una conversazione diretta o un gruppo: i messaggi continuano ad arrivare e il contatore dei non letti si aggiorna, ma senza popup, suoni né notifiche. L'impostazione è personale e non è visibile agli altri partecipanti.
Impostazioni dell'azienda
L'amministratore dell'azienda gestisce la chat dalle impostazioni del pannello, sezione Chat:
| Impostazione | Effetto |
|---|---|
| Chat attiva | Attiva o disattiva la chat per tutti gli interni dell'azienda |
| Invio di file | Consente o blocca l'invio di file nelle conversazioni |
| Conservazione dei messaggi | Per quanti giorni vengono conservati i messaggi: da 7 a 3.650, predefinito 365 giorni |
I messaggi più vecchi del periodo di conservazione vengono cancellati automaticamente ogni giorno. I file seguono sempre la propria regola delle 48 ore, indipendentemente dalla conservazione dei messaggi.
Cifratura end-to-end
Ogni messaggio e ogni file viene cifrato sul dispositivo di chi scrive e decifrato solo sui dispositivi dei partecipanti. Il centralino consegna e conserva soltanto contenuti cifrati: conosce chi scrive a chi e quando, ma non cosa.
Dispositivi della chat. Ogni browser in cui accedi al pannello e ogni dispositivo dell'app ha le proprie chiavi, create alla prima apertura della chat e custodite solo su quel dispositivo. Un messaggio viene cifrato per tutti i dispositivi collegati dei partecipanti, compresi gli altri tuoi dispositivi. Puoi collegare fino a 20 dispositivi per interno.
| Situazione | Cosa succede |
|---|---|
| Nuovo dispositivo o nuovo membro di un gruppo | Vede i messaggi da quel momento in poi; quelli precedenti restano leggibili sugli altri dispositivi |
| Esci dal pannello (logout) | Quel browser viene scollegato dalla chat e non riceve più messaggi; al prossimo accesso si ricollega |
| Dispositivo dell'app scollegato | Le sue chiavi della chat vengono revocate subito |
| Dispositivo perso | Scollegalo da Dispositivi della chat, oppure chiedi all'amministratore di scollegare tutti i dispositivi del tuo interno |
| Un collega cambia dispositivo | Nella conversazione compare «I dispositivi di Mario Rossi sono cambiati» |
Codice di sicurezza. In ogni conversazione diretta puoi aprire il codice di sicurezza (60 cifre e un QR) e confrontarlo di persona con il collega: se coincide, nessuno si è interposto tra voi.
Cosa comporta.
- La ricerca nei messaggi avviene sul tuo dispositivo.
- L'azienda ed Exobyte non possono leggere, recuperare o esportare le conversazioni.
- Le chiavi di cifratura si rinnovano automaticamente ogni 30 giorni.
- I messaggi di sistema dei gruppi (nuovi membri, uscite, cambio di nome) non sono cifrati: sono informazioni che il centralino conosce già.
Privacy
- Ogni conversazione è leggibile solo dai suoi partecipanti, grazie alla cifratura end-to-end. Nemmeno l'amministratore dell'azienda o Exobyte possono leggerla.
- Chi esce da un gruppo non riceve più i nuovi messaggi.
- I messaggi cifrati vengono conservati soltanto per consegnarli ai destinatari e per il periodo stabilito dall'azienda.
- Le comunicazioni tra pannello, app e centralino viaggiano inoltre su collegamenti cifrati.
Per gli sviluppatori
Le funzioni della chat sono disponibili via API, scope chat:read e chat:write. Ogni richiesta agisce come un interno: sessione del pannello di un utente con interno associato oppure token di un dispositivo dell'app ExoVoIP. Una credenziale senza interno associato riceve 403. Le impostazioni dell'azienda (PUT /v1/chat/settings) richiedono lo scope extensions:write.
| Descrizione | Richiesta | Scope | Metodo SDK |
|---|---|---|---|
| Interni dell’azienda con cui chattare e stato in linea | GET /v1/chat/directory | chat:read | chat.directory() |
| Conversazioni dell’interno (non lette, ultimo messaggio) | GET /v1/chat/conversations | chat:read | chat.listConversations() · py: chat.list_conversations() |
| Apre una conversazione diretta o crea un gruppo | POST /v1/chat/conversations | chat:write | chat.createConversation() · py: chat.create_conversation() |
| Dettaglio conversazione e membri | GET /v1/chat/conversations/:id | chat:read | chat.getConversation() · py: chat.get_conversation() |
| Gruppo: nome e membri; silenzia le notifiche | PATCH /v1/chat/conversations/:id | chat:write | chat.updateConversation() · py: chat.update_conversation() |
| Esce da un gruppo (le dirette si nascondono) | DELETE /v1/chat/conversations/:id | chat:write | chat.leaveConversation() · py: chat.leave_conversation() |
| Chiavi dei dispositivi dei partecipanti (destinatari esatti di un messaggio) | GET /v1/chat/conversations/:id/keys | chat:read | chat.conversationKeys() · py: chat.conversation_keys() |
| Messaggi della conversazione (dal più recente) | GET /v1/chat/conversations/:id/messages | chat:read | chat.listMessages() · py: chat.list_messages() |
| Invia un messaggio cifrato (busta con un wrap per ogni dispositivo attivo dei partecipanti) | POST /v1/chat/conversations/:id/messages | chat:write | chat.sendMessage() · py: chat.send_message() |
| Invia un file cifrato (massimo 20 MB, disponibile 48 ore) con la sua busta | POST /v1/chat/conversations/:id/files | chat:write | chat.sendFile() · py: chat.send_file() |
| Elimina un proprio messaggio (e il file allegato) | DELETE /v1/chat/conversations/:id/messages/:messageId | chat:write | chat.deleteMessage() · py: chat.delete_message() |
| Segna come letto fino al messaggio indicato | POST /v1/chat/conversations/:id/read | chat:write | chat.markRead() · py: chat.mark_read() |
| Link di download monouso (60 secondi) per un file cifrato | POST /v1/chat/files/:id/ticket | chat:read | chat.fileTicket() · py: chat.file_ticket() |
| Scarica un file cifrato (autenticato; anche con ?ticket=) | GET /v1/chat/files/:id | chat:read | chat.downloadFile() · py: chat.download_file() |
| Biglietto per il collegamento in tempo reale (30 secondi) | POST /v1/chat/ws-ticket | chat:read | chat.wsTicket() · py: chat.ws_ticket() |
| Collega questo dispositivo alla chat (chiavi pubbliche firmate) | POST /v1/chat/keys | chat:write | chat.registerKey() · py: chat.register_key() |
| Chiavi attive degli interni indicati, oppure chiavi indicate in qualsiasi stato (verifica dei messaggi passati) | GET /v1/chat/keys | chat:read | chat.listKeys() · py: chat.list_keys() |
| I miei dispositivi della chat (anche chiavi sostituite) | GET /v1/chat/keys/mine | chat:read | chat.myKeys() · py: chat.my_keys() |
| Rinnova la chiave di cifratura del dispositivo (stessa identità) | POST /v1/chat/keys/:keyId/rotate | chat:write | chat.rotateKey() · py: chat.rotate_key() |
| Scollega un mio dispositivo dalla chat | DELETE /v1/chat/keys/:keyId | chat:write | chat.revokeKey() · py: chat.revoke_key() |
| Dispositivi della chat di un interno (amministratore) | GET /v1/chat/extensions/:id/keys | extensions:read | chat.extensionKeys() · py: chat.extension_keys() |
| Scollega dalla chat tutti i dispositivi di un interno (dispositivo perso) | DELETE /v1/chat/extensions/:id/keys | extensions:write | chat.revokeExtensionKeys() · py: chat.revoke_extension_keys() |
| Impostazioni chat dell’azienda | GET /v1/chat/settings | chat:read | chat.getSettings() · py: chat.get_settings() |
| Attiva la chat, invio file, conservazione dei messaggi (amministratore) | PUT /v1/chat/settings | extensions:write | chat.setSettings() · py: chat.set_settings() |
Cifratura end-to-end e chiavi dei dispositivi
La specifica completa (algoritmi, formati, vettori di prova) è nel documento _Chat interna: cifratura end-to-end_; la libreria di riferimento TypeScript è @exopbx/chat-crypto. In sintesi:
- ogni dispositivo registra le proprie chiavi pubbliche con
POST /v1/chat/keys:identityKey(Ed25519),encryptionKey(X25519),createdAt,signature(firma Ed25519 del bundle),platformelabel. Il centralino verifica la firma e restituiscekey.keyId; POST /v1/chat/keys/:keyId/rotaterinnova la chiave di cifratura con la stessa identità (la chiave precedente diventasuperseded);DELETE /v1/chat/keys/:keyIdscollega il dispositivo;GET /v1/chat/keys/mineelenca i propri dispositivi;GET /v1/chat/keys?extensionIds=12,15restituisce le chiavi attive degli interni;GET /v1/chat/keys?keyIds=31,40restituisce le chiavi indicate in qualsiasi stato (anche sostituite o scollegate), per verificare la firma dei messaggi passati;GET /v1/chat/conversations/:id/keysle chiavi che devono ricevere il prossimo messaggio;- l'amministratore vede e scollega i dispositivi di un interno con
GET/DELETE /v1/chat/extensions/:id/keys.
Il dispositivo indica la propria chiave con l'header X-Exo-Chat-Key: <keyId> su tutte le richieste della chat e su POST /v1/chat/ws-ticket: ogni messaggio arriva con il solo wrap di quel dispositivo. Una chiave revocata o di un altro dispositivo risponde 403 con codice CHAT_KEY_INVALID.
Messaggi e file
POST /v1/chat/conversations con { "type": "direct", "extensionId": 12 } restituisce la conversazione diretta esistente o ne crea una; con { "type": "group", "name": "Assistenza", "extensionIds": [12, 15] } crea un gruppo. PATCH accetta name, addExtensionIds, removeExtensionIds e muted.
I messaggi si leggono dal più recente, fino a 100 per pagina (limit, predefinito 50): before=<id> per le pagine precedenti, after=<id> per recuperare quelli arrivati dopo un'interruzione. Ogni messaggio ha kind (text, file, system), envelope (busta cifrata con il wrap del dispositivo che chiede), body (solo per i messaggi di sistema), attachment (id, dimensione cifrata, expiresAt, expired) e clientId.
L'invio è una busta cifrata per esattamente le chiavi attive dei partecipanti, mittente compreso:
{
"envelope": {
"v": 1,
"senderKeyId": 15,
"ephemeral": "<base64url>",
"nonce": "<base64url>",
"ciphertext": "<base64url>",
"signature": "<base64url>",
"recipients": [{ "keyId": 15, "nonce": "<base64url>", "wrapped": "<base64url>" }]
},
"clientId": "c7f1b2e9a4d0"
}Se l'elenco dei destinatari non coincide con le chiavi attive, la risposta è 409 con codice CHAT_KEYS_CHANGED e details { "missing": [...], "extra": [...] }: il client ricarica le chiavi e cifra di nuovo. Limiti: testo cifrato fino a 24 KB, al massimo 1.000 destinatari.
Il campo facoltativo clientId (8–64 caratteri A-Z a-z 0-9 _ -) è generato dal client e permette di ritentare un invio senza duplicare il messaggio.
POST /v1/chat/conversations/:id/files è una richiesta multipart/form-data con il campo envelope (JSON della busta, che contiene nome, tipo, impronta e chiave del file), il campo file (il file già cifrato a blocchi) e il facoltativo clientId. Il file cifrato può superare i 20 MB solo dei byte di controllo dei blocchi:
curl -s https://centralino.exovoip.it/api/v1/chat/conversations/42/files \
-H "Authorization: Bearer $TOKEN" \
-F "envelope=<busta.json" \
-F "file=@preventivo.pdf.enc;type=application/octet-stream" \
-F "clientId=c7f1b2e9a4d0"I file si scaricano sempre come application/octet-stream e vanno decifrati dal client. Per un download da un contesto che non può inviare header si chiede un link con POST /v1/chat/files/:id/ticket: la risposta contiene url (indirizzo assoluto) ed expiresAt; il link vale 60 secondi, una sola volta, solo per chi lo ha richiesto. I file non più disponibili rispondono 410.
Tempo reale (WebSocket)
Il collegamento in tempo reale si apre in due passi: POST /v1/chat/ws-ticket (con l'header X-Exo-Chat-Key) restituisce un biglietto monouso valido 30 secondi, poi si apre wss://…/v1/chat/ws?ticket=<biglietto>. Ogni messaggio del WebSocket è un oggetto JSON con il campo type. Alla connessione arriva ready:
{ "type": "ready", "extensionId": 12 }Un nuovo messaggio (cifrato, con il wrap di questo dispositivo):
{
"type": "message.new",
"message": {
"id": 9812,
"conversationId": 42,
"senderExtensionId": 15,
"kind": "text",
"body": null,
"envelope": {
"v": 1,
"senderKeyId": 31,
"ephemeral": "<base64url>",
"nonce": "<base64url>",
"ciphertext": "<base64url>",
"signature": "<base64url>",
"wrap": { "keyId": 27, "nonce": "<base64url>", "wrapped": "<base64url>" }
},
"attachment": null,
"clientId": null,
"createdAt": "2026-11-04T10:15:00.000Z",
"deletedAt": null
}
}| Evento | Quando |
|---|---|
ready | Connessione stabilita |
message.new | Nuovo messaggio in una conversazione dell'interno (anche i propri) |
message.deleted | Messaggio eliminato dal mittente |
conversation.updated | Nuova conversazione, cambio di nome o membri, silenziamento |
conversation.removed | L'interno è uscito o è stato tolto da un gruppo |
read | Un partecipante ha letto fino a messageId |
typing | Un partecipante sta scrivendo (typing: true) o ha smesso (false) |
presence | Cambio dello stato in linea di un interno |
attachment.expired | Un file ha superato le 48 ore ed è stato cancellato |
keys.changed | I dispositivi di un interno sono cambiati: ricaricare le sue chiavi |
pong | Risposta a ping |
Il client può inviare { "type": "typing", "conversationId": 42, "typing": true }, { "type": "presence", "presence": "away" } (oppure online) e { "type": "ping" }. Se la chiave del dispositivo viene revocata la connessione si chiude con il codice 4403.
const { ticket } = await fetch(`${base}/v1/chat/ws-ticket`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'X-Exo-Chat-Key': String(keyId) },
}).then((r) => r.json());
const ws = new WebSocket(`${base.replace(/^http/, 'ws')}/v1/chat/ws?ticket=${ticket}`);
ws.onmessage = (e) => {
const ev = JSON.parse(e.data);
if (ev.type === 'message.new') console.log(ev.message.conversationId, ev.message.envelope?.wrap);
};Ogni riconnessione richiede un nuovo biglietto. Dopo una riconnessione si riallineano le conversazioni con GET /v1/chat/conversations e i messaggi con after=<ultimo id ricevuto>.