Danielside

informática softwarelibre divagaciones música

Puente Telegram <> XMPP, pero bien

Hace ya tiempo que tenía ganas de acometer un verdadero rediseño del puente telegram <> xmpp basado en telegram-cli. La verdad es que Telegram lleva la compatibilidad hacia atrás como bandera y me sorprende que después de bastantes años abandonado, este cliente siguiera funcionando. Con achaques, pero seguía funcionando.

Había llegado a un punto en el que no podía recibir imágenes, vídeos o documentos desde contactos Telegram con un cliente moderno, ya que telegram-cli no estaba preparado para procesarlos. Y audios nunca pude recibir. Para solucionarlo, tenía que crear mi propio cliente.

Se puede acometer la creación de un cliente Telegram completo (no un bot) mediante Telegram API o mediante TDLib. La primera opción es compleja, porque conlleva bregar con el protocolo a más bajo nivel, MTProto (esto es lo que en su momento hizo el proyecto telegram-cli).

Para simplificar las cosas, Telegram montó una API basada en intercambio de mensajes JSON, TDLib, que viene de Telegram Database Library. Y esto simplifica la misión… pero solo si eres capaz de enterarte de como empezar. No es algo que esté claramente explicado (en mi opinión) por eso cuento mi experiencia con TDLib.

Lo primero que se me vino a la cabeza cuando vi que existía una API JSON es lo típico ¿donde está la definición de los mensajes, en una página tipo Swagger? Pero no funciona así. Telegram lo que hace es crear una base de datos local (que puede ser simplemente un conjunto de directorios y ficheros). De la interacción con esa base de datos local se ocupa exclusivamente una librería escrita en C, llamada también TDLib, que hay que compilar previamente. Esa librería ofrece una serie de métodos, los cuales serás capaz de usar a condición de que el lenguaje de programación que se emplee para el cliente sepa llamar a funciones C. La librería C TDLib será lo suficientemente inteligente como para pedir al servidor remoto de Telegram todo lo que no sea capaz de encontrar en la base de datos local.

Ese básicamente es el meollo de la cuestión: 1) compilas TDLib en la plataforma que te interese, 2) eliges un lenguaje de programación que sepa invocar funciones C y 3) tratas eventos de mensajes entrantes y envías mensajes salientes de/hacia TDLib. El resultado final de mi cliente/puente escrito en Python está aquí: https://gitlab.com/danielside/xmpptelegrampy

Compilando TDLib

La inteligencia de los creadores de la API reside en crear una librería que haga todo el trabajo duro del protocolo a bajo nivel, y que pueda usar desde cualquier lenguaje de programación capaz de trabajar con JSON y con funciones C. Viendo el directorio de ejemplos del repositorio GIT, se ve que hay una gran variedad de lenguajes que cumplen estos requisitos.

Han creado un asistente de compilación para cualquier plataforma y lenguaje de programación empleado que facilita mucho la tarea. Yo recomiendo compilarlo en un sistema con al menos 4G de RAM, porque la opción que indica el asistente de preparar para compilar en sistemas con menos RAM, a mi me ha dado errores.

Cuando termina la compilación lo único que necesitas es el archivo .so resultante. A mi me gusta conservar la versión, por tanto en el directorio lib del repo he incluido TDLib versión 1.7.10 compilado para Debian 10 64bit.

Uso desde Python

El estado de implementación de las librerías wrapper de TDLib es muy variado. Mi punto de partida para el puente Telegram <> XMPP ha sido python-telegram, por dos motivos: no tenía ni idea de como empezar solo con la librería C compilada y 2) es un cliente básico que permite enviar y recibir texto a la vez que tiene funciones de más bajo nivel que permiten enviar o recibir cualquier otra cosa.

Si analizamos un poco la librería python-telegram y el fichero de configuración de XmppTelegramPy podemos ver que es necesario especificar la ruta del fichero .so para que Python pueda cargar la librería y llamar a sus funciones. Lo que hace python-telegram es realizar un binding de las funciones C a funciones equivalentes en Python. La librería C ofrece una interfaz bastante simple, con funciones para enviar y recibir, que están detalladas en la documentación de TDLib. Por ejemplo, una cabecera es:

void td_json_client_send (void *client, const char *request)

Y cuando realiza el binding, python-telegram dispone de la siguiente función Python:

def _send_data(
    self,
    data: Dict[Any, Any],
    result_id: Optional[str] = None,
    block: bool = False,
) -> AsyncResult:
    """
    Sends data to tdlib.
    If `block`is True, waits for the result
    """

Además de un método de más alto nivel como send_message, que compone (este sí) una cadena JSON y la envía utilizando _send_data.

Lo primero que hay que comprender de la librería “pelada” TDLib es que no trae métodos para enviar mensajes, fotos, etc. Solo trae métodos para comunicarse con el servidor (que no es poco). Y lo segundo es que en función del lenguaje a emplear puedes encontrar integraciones más o menos sofisticadas. En concreto python-telegram solo trae método de alto nivel para enviar mensajes de texto, pero al menos me dio pie a entender algo: cómo saber qué tipos de mensajes JSON se intercambian, es decir, cual es el vocabulario.

Enviar y recibir contenido

Como decía, la pata que faltaba para saber como enviar y recibir contenido tangible es comprender como montar e interpretar los mensajes JSON.

Lo que hace la librería TDLib es simplemente serializar objetos. Estos objetos son instancias de las clases que se encuentran definidas en la cabecera de td_api.

A modo de ejemplo, y como no venía hecho en python-telegram, tuve que averiguar como enviar una foto a un chat de Telegram. Lo primero que tenemos que hacer es mirar la documentación para la clase sendMessage.

Una vez vistos los parámetros que admite, solo tenemos que serializar una instancia de de sendMessage en el formato JSON admitido por TDLib:

photo = tg.call_method('sendMessage', params={
    'chat_id': user_chats[chat_name][0],
    'input_message_content': {
    '@type': 'inputMessagePhoto',
    'photo': {
        '@type': 'inputFileLocal',
        'path': path
     }
}}, block=True)

La función call_method está definida en python-telegram y lo que hace es montar un diccionario que servirá de base para el JSON que se envíe:

data = {'@type': method_name}

if params:
    data.update(params)

return self._send_data(data, block=block)

Como se puede observar, todos los mensajes JSON intercambiados siempre tienen un primer elemento llamado @type, donde se codifica precisamente esa clase que estamos serializando. A su vez, los miembros de esa clase pueden ser valores escalares (enteros, cadenas, etc) o instancias de otras clases. Por eso en el fragmento donde llamo a call_method se ve como especifico el tipo de mensaje que quiero enviar, que es un inputMessagePhoto.

En resumen: estudiar la interfaz de td_api y crear los documentos JSON pasando exactamente los miembros de objeto que necesita cada uno de ellos.

Para procesar los mensajes entrantes funciona de manera similar, solo que hay que consultar td_api para saber qué tipos de campos podemos esperar en cada evento.

XmppTelegramPy

El puente usa dos threads: el principal se queda esperando eventos Telegram, y se abre uno adicional para crear un socket que se queda escuchando en un puerto. Este socket recibe comandos desde Prosody y se encargará de enviar mensajes de texto y fotos a contactos Telegram. El encargado de enviar los comandos es el (remozado también) módulo telegram en Lua para Prosody. Es “remozado” porque me he librado de la dependencia que tenía con el programa netcat para comunicarme con el socket y lo hago directamente mediante Lua.

Esta nueva versión del puente también me ha permitido librarme de la dependencia del programa sendxmpp de Linux para enviar los mensajes Telegram hacia el contacto XMPP. Ahora realizo esa comunicación usando la librería aioxmpp. Por el momento, los contenidos no textuales los copio al directorio de http de Prosody y los envío al contacto XMPP como una URL, pero al superar la limitación respecto de sendxmpp ahora podría enviar stanzas más elaboradas a futuro.

En esta captura podemos ver algunas comunicaciones desde Telegram hacia Gajim:

  • Cada mensaje lleva un icono unicode que informa sobre el tipo de mensaje.
  • Los “GIF” no son realmente GIF, son vídeos MP4 cortos.
  • Los sticker telegram -como la cereza- son animaciones vectoriales Lottie un poco engorrosas de tratar. Pero los programadores de Telegram han sido muy hábiles y siempre hay un carácter Unicode equivalente y por ahora eso es lo que muestro. El carácter equivalente viene en el mismo mensaje JSON donde viene el sticker.
  • Los documentos y los vídeos llevan una indicación del tamaño. Esto te da la oportunidad de no descargarlos si vas corto de datos.

En el README hay instrucciones para hacerlo funcionar. Debe tener permisos para escritura en el directorio de descargas de Prosody y se debe crear el servicio SystemD o bien arrancarlo simplemente con python3 main.py. Permite un login desatendido enviando las credenciales a ficheros temporales que él mismo se encarga de eliminar.

En esta otra captura vemos como se envían mensajes y fotos a un contacto XMPP y llegan al chat Telegram:

Telegram no es la panacea de seguridad y privacidad con respecto a alternativas que todos conocemos, pero al menos los clientes y las librerías oficiales son software libre y nos da la oportunidad de crear clientes alternativos. Esto por ejemplo te daría la oportunidad de crear un cliente no oficial donde se usa la red de Telegram pero con un cifrado punto a punto implementando por ti mismo. Por eso, aunque al usar Telegram no controlamos los servidores y por tanto tampoco es 100% fiable, el hecho de que sea software libre y que permita crear clientes alternativos lo sitúa a años luz de la archiconocida.

Para usar la API necesitarás credenciales fáciles de conseguir. Como consejo final y para que no te baneen de por vida, en algún sitio leí que es bueno dejarles saber (mediante correo a recover@telegram.org) que estás trabajando con la API, cual es tu teléfono y posiblemente la IP desde la que recibirán las conexiones. Porque si te cortan el acceso es difícil salir de la lista negra.


¿Por qué “Database Library”? Toda la información del cliente, tanto la autenticación como los ficheros, residen en una base de datos. Por defecto es una estructura de directorios:

Lo interesante es que (cerrando previamente el cliente) puedes eliminar en cualquier momento esta estructura por completo y la volverá a crear en el siguiente login. Si al arrancar XmppTelegramPy existe la base de datos y tiene información de autenticación válida (database/secret) no pedirá de nuevo código de inicio y contraseña (si la tienes habilitada). Pero si no existe la pedirá y creará la base de datos completa que ves ahí.


Archivado en categoría(s) XMPP

Enlace permanente



Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.