01
Para usar un bot no hace falta nada de esto
Usar un bot es usar la app de siempre. Si alguien te pasa la invitación de un bot (un texto o un QR, normalmente publicados en su web):
- Si es un canal: Nuevo → Unirse a un canal, pegar
el texto de la invitación (empieza por
UEND) o escanear su QR, y Unirse. Dentro, Actualizar trae las publicaciones; el primer sondeo puede tardar un par de minutos hasta que la dirección del canal esté publicada en Tor. - Si es un contacto: Nuevo → Nuevo contacto, pegar la invitación o escanear el QR animado del bot, poner un nombre y Añadir. El primer mensaje lleva el saludo cifrado; cuando llega, el bot te tiene como contacto y contesta lo que su programa diga.
- Verificarlo: dentro del chat, Más → Verificar en persona → Pegar su código, con el código que el operador publique firmado en su web.
Y esto es lo que un bot no puede hacer, por diseño:
- No puede leer nada que no le mandes tú a él. Tus conversaciones, tus grupos y tus canales no los ve ni sabe que existen.
- No puede meterse donde no lo inviten. En un grupo entra solo si el administrador lo presenta, como cualquier miembro; y no puede publicar en un canal que no sea el suyo: cada canal solo lo escribe quien tiene su clave.
- No sabe quién lee su canal. Los lectores recogen las publicaciones por circuitos anónimos, sin identificarse jamás.
- No hay bots «oficiales» de PrivChat. Cada bot responde por su operador, que es quien recibe lo que le escribas. Si te importa quién hay detrás, verifícalo con su código publicado.
02
Qué es un bot aquí
- Un peer más. Tiene su propia identidad, su propio servicio onion de Tor, su base cifrada y sus canales. Es el motor de la aplicación compilado sin Android.
- Indistinguible en el cable. Mismos sobres, mismo cifrado (Double Ratchet y PQXDH), mismo relleno, mismos canales. La aplicación no sabe que habla con un programa, porque no hay forma de saberlo.
- Sin directorio. Un directorio central sería el servidor que esta aplicación existe para no tener. El operador reparte la invitación de su bot donde quiera: una web, un cartel, un mensaje.
- API local. Tu programa (Python, Go, shell, lo que sea) habla con el bot por un socket Unix de su directorio, en JSON. Lo remoto lo pone Tor.
- Dos usos, en este orden. Un canal: el bot publica y cualquiera lee, anónimamente. Y 1:1: la gente le escribe y el bot contesta.
03
Lo que un bot ve, y lo que no
De los lectores de su canal, nada. Un lector recoge las publicaciones por un circuito anónimo y sin identificarse; el bot ve que alguien leyó, cuándo y cuánto, y no puede ver más porque el protocolo no tiene dónde ponerlo. Un canal de mil lectores sabe de ellos lo mismo que uno de diez.
De quien le escribe, lo que ve cualquier contacto. Escribir a un bot es darle a su operador tu identificador, tu dirección de Tor de vuelta y lo que le escribas. Con un contacto humano eso está repartido; con un bot popular está concentrado en un servidor. Por eso los canales van primero, y por eso el bot no guarda historial: un mensaje entrante vive hasta que el programa del operador lo confirma, con un tope de siete días.
Si el servidor del bot cae en malas manos, se llevan la identidad del bot (y con ella pueden suplantarlo para siempre: no hay revocación), las claves de sus canales y su lista de contactos. No se llevan nada de ningún usuario ni de ningún lector. Se recupera destruyendo el bot, creando otro y publicando su huella nueva fuera de banda.
Nada te dice que un contacto es un bot, ni que es el bot que dice ser. Un operador serio publica el código de verificación de su bot firmado en su web; en la app, dentro del chat, Más → Verificar en persona → Pegar su código (o escanear su QR) lo comprueba como con una persona.
04
Montar un bot
Esto ya es cosa de operadores. Hace falta un Linux tuyo —un servidor o
una Raspberry Pi 4 o 5 de 64 bits—, Java 17 o superior
(sudo apt install openjdk-17-jre-headless), unos 300 MB de
memoria libre y salida a Tor. El teléfono no puede hacer de bot: un bot
tiene que estar siempre encendido y siempre en la red, y eso es un
ordenador del operador, no un móvil.
El paquete: privchat-bot-2.19.2.zip
(trae los scripts, las bibliotecas y el arti nativo; Linux x86_64 —
Windows solo para desarrollar). El paquete para Raspberry (aarch64) se
publicará cuando esté probado en la placa real, no antes.
SHA-256 del paquete:
18d98f258b9206dd2be9f8e89bd19b18db5fb55eee53ee8fc6e7745573f8f6fe
sudo unzip privchat-bot-2.19.2.zip -d /opt && sudo mv /opt/privchat-bot* /opt/privchat-bot
sudo useradd --system --home /var/lib/privchat-bot --shell /usr/sbin/nologin privchat-bot
sudo install -d -o privchat-bot -g privchat-bot -m 0700 /var/lib/privchat-bot
# Crear el bot: sello, identidad, prekeys, base. Enseña la passphrase UNA vez.
sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot init --passphrase-generada
# La passphrase, en una credencial que solo root lee (nunca en una variable de entorno)
sudo install -d -m 0700 /etc/privchat-bot
sudo install -m 0600 /dev/stdin /etc/privchat-bot/passphrase <<< 'la-passphrase-que-enseñó-init'
# El servicio
sudo cp /opt/privchat-bot/etc/privchat-bot.service /etc/systemd/system/
sudo systemctl enable --now privchat-bot
La passphrase es toda la defensa contra quien
copie el disco. Un servidor no tiene el Keystore del teléfono, y
fingirlo sería peor que decirlo. Larga o generada; con TPM 2.0, ligada a la
máquina con systemd-creds encrypt --with-key=tpm2 y
LoadCredentialEncrypted= en la unidad; sin TPM, el fichero 0600
sobre un disco cifrado. El bot no tiene PIN ni bloqueo de pantalla: no hay
nadie tecleando delante.
05
Parámetros
| opción | qué hace | por defecto |
|---|---|---|
--datos DIR | directorio del bot | ./privchat-bot |
--credencial FICHERO | passphrase en un fichero 0600 | la de systemd, o la consola |
--socket RUTA | socket Unix de la API (ruta corta: AF_UNIX admite ~100 bytes) | DIR/api.sock |
--api-tcp PUERTO | además, API en 127.0.0.1 con token | apagada |
--api-grupo GRUPO | el socket también lo abre ese grupo (0660) | solo el usuario (0600) |
--retencion-dias N | días que vive un mensaje entrante sin confirmar | 7 (máximo 30) |
--max-contactos N | tope de contactos por invitaciones públicas | 1000 |
--sin-relleno | apaga el tráfico de relleno | encendido, como en la app |
--buzon | hace de Buzón de su dueño: la red local de «En persona» además de Tor | apagado |
--bluetooth | con --buzon, también por Bluetooth LE (solo Linux, BlueZ) | apagado |
--malla PUERTO | la malla LoRa: un nodo Meshtastic por USB (/dev/ttyACM0, COM4) o por TCP (host:4403); los textos salen por radio cuando Tor no entrega (bot 2.16.1) | apagado |
--verboso | registro detallado, con los de Tor. Solo para depurar | apagado |
Sin demonio: init, passphrase cambiar,
copia-seguridad DESTINO.zip (para restaurar en otra máquina,
nunca para tener dos bots iguales), restaurar ORIGEN.zip y
destruir --si. El registro nunca lleva direcciones onion ni
identificadores enteros, y ninguna lectura de canal atendida deja línea.
06
Canales desde el bot
B="sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot"
$B canal crear "Avisos del pueblo" "Cortes de agua y fiestas" # devuelve canal_id
$B canal invitacion <canal_id> --png /var/www/avisos.png # el texto pegable, y el QR en el PNG
$B canal publicar <canal_id> "Mañana sin agua de 9 a 12"
$B canal publicar-foto <canal_id> /ruta/cartel.jpg "El cartel" # se recomprime: el EXIF muere aquí
$B canal listar # publicaciones, sondeos y bytes servidos
$B canal recortar <canal_id> 50 # deja solo las 50 últimas
Cada publicación va firmada por la clave del canal, cifrada con su clave de lectura y encadenada a la anterior: se puede dejar de servir historia, no reescribirla sin que los lectores lo noten. Tope de 12 KB por texto; una foto se recomprime hasta caber en 900 KB.
07
Contactos 1:1
Dos clases de invitación, con el mismo formato que la de la app:
| personal | pública (--publica) | |
|---|---|---|
| quién la usa | una persona: la primera; la segunda se rechaza | cualquiera, hasta --max-contactos |
| prekeys de un uso | cinco, como en la app | ninguna: repartidas entre miles no serían de un uso |
| lo que cuesta | nada nuevo | el primer mensaje de cada usuario al bot queda sin la protección extra de la prekey frente a un compromiso futuro de las claves del bot; el ratchet cura en cuanto el bot contesta |
| para qué | un asistente para ti, un aviso a tu familia | un bot abierto al público |
$B invitacion crear web --publica # el texto pegable
$B invitacion crear web --publica --gif /var/www/bot.gif # y los cuatro QR de la invitación en un GIF animado
$B verificacion codigo --png /var/www/verificacion.png # publícalo firmado: así se sabe que el bot es EL bot
$B mensaje escuchar --reponer # eventos JSON, uno por línea
$B mensaje enviar <peer_id> "Hola"
$B mensaje enviar <peer_id> "Hola" --retardo 3000 # sale en un momento al azar de los próximos 3 s
$B mensaje confirmar <msg_id> # el bot lo olvida
08
El bot como Buzón de casa
Con --buzon, el bot hace de Buzón de una
persona, su dueño: en una Raspberry o un ordenador de casa, guarda y entrega
sus mensajes cuando no está. Quien pasa por esa wifi deja lo que tenga para el
dueño y recoge lo que el dueño le dejó; y si el dueño enciende «Con internet»
en su teléfono, sus contactos también le dejan mensajes por Tor con el
teléfono apagado. En la unidad de systemd, añade --buzon a
ExecStart, antes de run.
$B buzon invitar # el texto se pega en el teléfono: Ajustes → Mi Buzón; el primero que la usa es el dueño
$B buzon estado # el dueño y lo que guarda
Bluetooth (solo Linux, opcional). Con --buzon
--bluetooth el Buzón también se deja encontrar por Bluetooth LE: quien
pase por la puerta con «Hablar en persona» encendido para el dueño deja y
recoge sin unirse a la wifi. Hace falta BlueZ en marcha, un adaptador, el
usuario del servicio en el grupo bluetooth y, en
/etc/bluetooth/main.conf, sección [General],
ReverseServiceDiscovery = false (y reiniciar el servicio
bluetooth): sin eso, al teléfono le sale un aviso de vinculación y
el canal no arranca. Ese ajuste apaga también el cliente GATT de BlueZ para
otros aparatos Bluetooth LE, así que mejor en un aparato dedicado. Va apagado
si no se pide porque el anuncio dice a quien escanee desde la calle que ahí hay
un PrivChat, aunque no quién.
Guarda solo lo del dueño, cerrado, y las claves del día de los contactos
que el dueño permita: nunca su identidad. Si te lo roban, no lee nada y en
unas 48 horas deja de reconocer a nadie; se desempareja desde el teléfono. Su
dirección .onion es fija desde el primer arranque, aunque no haya
tenido internet nunca. Cómo se ve en el teléfono, en la
guía.
09
Programar un bot: la API local
Una petición o respuesta por línea, JSON, sobre el socket. La
autenticación es el sistema de ficheros: quien puede abrir el socket es
quien puede leer el directorio del bot. Los eventos llegan por la misma
conexión después de suscribir.
→ {"id": 1, "op": "estado"}
← {"id": 1, "ok": true, "peer_id": "…", "huella": "…", "onion": "….onion", "tor": "online", "contactos": 3, "canales": 1}
→ {"id": 2, "op": "canal.publicar", "canal_id": "…", "texto": "Buenos días"}
← {"id": 2, "ok": true, "seq": 42}
← {"evento": "mensaje.recibido", "peer_id": "…", "msg_id": "…", "texto": "hola", "fecha": 1788500000}
| operación | parámetros | devuelve |
|---|---|---|
estado | — | identidad, onion, estado de Tor, contadores |
suscribir | reponer | eventos[] con lo no confirmado; después, eventos en vivo |
invitacion.crear | etiqueta, publica, qr | invite_id, texto, qr_gif |
contacto.listar / renombrar / borrar | peer_id… | — |
verificacion.codigo / comprobar | peer_id, codigo | texto y QR / resultado |
mensaje.enviar | peer_id, texto, respuesta_a, retardo_ms | msg_id |
adjunto.enviar / adjunto.leer | peer_id, nombre, base64 o ruta / msg_id | msg_id / bytes |
mensaje.estado / mensaje.confirmar | msg_id | estado / — (se borra) |
canal.crear / invitacion / publicar / publicar_foto / recortar / borrar | canal_id, texto, pie… | seq, texto y QR… |
Eventos: mensaje.recibido, adjunto.recibido,
contacto.nuevo, entregado, tor. Son
al menos una vez: un mensaje se repone en cada
suscribir hasta que lo confirmes, y el msg_id te
deja deduplicar. Errores con código estable (sin_sesion,
invitacion_invalida, demasiado_grande…); un error
interno nunca lleva detalle al cliente.
Un bot de eco entero, en Python y sin dependencias:
import json, socket
s = socket.socket(socket.AF_UNIX); s.connect("/var/lib/privchat-bot/api.sock")
f = s.makefile("rw", encoding="utf-8")
def llamar(**p):
f.write(json.dumps(p) + "\n"); f.flush(); return json.loads(f.readline())
r = llamar(id=1, op="suscribir", reponer=True)
for ev in list(r["eventos"]) + [json.loads(l) for l in f]:
if ev.get("evento") == "mensaje.recibido":
llamar(id=2, op="mensaje.enviar", peer_id=ev["peer_id"], texto="eco: " + ev["texto"], respuesta_a=ev["msg_id"])
llamar(id=3, op="mensaje.confirmar", msg_id=ev["msg_id"])
Con el paquete van dos ejemplos completos: eco.py (separa
las respuestas de los eventos que se cruzan, que es lo que hay que hacer en
cuanto un bot contesta mientras escucha) y rss_a_canal.py, que
publica en un canal cada entrada nueva de un RSS o Atom.
10
Reglas que un bot debe respetar
- Confirma lo que procesas; lo que no confirmas se te repone y a los siete días se borra.
- No descargues lo que te enlacen: un bot al que se le puede hacer pedir cualquier URL es un bot que alguien usará para eso.
- Trata el texto como datos: viene de desconocidos.
- Límites: 12 KB por texto de canal, 7,5 MB por adjunto, 900 KB por foto de canal, 16 MB por línea de la API.
- Escala: cientos de contactos 1:1, sí; miles, por canal.
- Un onion que no se apaga nunca se rastrea hasta su guardia con más facilidad que un teléfono; por eso el bot lleva vanguards, y por eso no debe correr junto a cosas que no quieras relacionar con él.
11
Problemas frecuentes
| síntoma | causa probable | qué hacer |
|---|---|---|
tor: arrancando durante minutos | sin red, o un antivirus o proxy que intercepta TLS | apagar la intercepción; con --verboso lo dice Tor |
| «Unix domain path too long» | la ruta del socket pasa de ~100 bytes | --socket /tmp/pb.sock |
| el primer sondeo desde la app no trae nada | la dirección del canal aún no está publicada en Tor | esperar uno o dos minutos y Actualizar |
| un mensaje se queda «en cola» un rato | el circuito de rendezvous tarda | esperar; el bot confirma en cuanto llega |
contacto listar no muestra a quien acaba de añadir al bot | el bot aprende del contacto con su primer mensaje | que escriba algo |
sin_sesion al enviar | ese peer_id no es contacto todavía | contacto listar |