Cómo usar Telegram para enviar notificaciones push desde Symfony

En varios proyectos nos hemos encontrado con la necesidad de enviar notificaciones en las que el clásico email se nos queda corto. ¿Por qué? Porque el receptor tiene que leer la notificación inmediatamente en su móvil, y realizar una acción en unos pocos minutos para que se produzca una venta.

Este es el caso de varios marketplaces de contratación de servicios, en los que el cliente solicita disponibilidad a un profesional y este debe responder en menos de una hora si está disponible o no. Usando un email, muchas transacciones se quedaban sin realizar, simple y llanamente porque el profesional no había leído el email a tiempo: sí, reconozcámoslo, recibimos decenas de emails al día y aunque nos lleguen al móvil, no les hacemos mucho caso.

Para solventar este problema y conseguir notificaciones push que «llamen la atención» del receptor, tenemos varias opciones:

  • SMS: opción clásica y segura, pero con muchas limitaciones. Aparte del límite de 160 caracteres por envío, es necesario contratar previamente un paquete con un número fijo de mensajes (que tendremos que renovar periódicamente) y si el número de notificaciones es importante, puede que no sea rentable. Por ejemplo, un paquete de 50.000 SMS nos costaría 3.500 euros.
  • Aplicación nativa: tiene el problema de que, aparte de las notificaciones, ¿qué más ofrecemos en la aplicación? No tiene sentido una aplicación que solo nos sirva para realizar notificaciones y nada más. Aparte, requiere de una importante inversión en desarrollo y mantenimiento tanto para Android como para iOS (a pesar de que que hoy en día tengamos virguerías como React Native).
  • WhatsApp: todo el mundo usa WhatsApp, por lo que sería el candidato ideal. Pero no ofrece ninguna API pública, y la única forma de interactuar con él es con APIs no oficiales que ofrecen nula seguridad de cara al futuro.
  • Telegram: ofrece una API pública y oficial que nos permite trabajar con bastantes garantías. También disponemos de la funcionalidad de los bots, que nos permiten enviar un mensaje de forma masiva, aunque no nos sirve para nuestro caso, donde necesitamos enviar notificaciones individuales a una persona en concreto. El uso de Telegram no está todavía tan extendido como WhatsApp, pero no es un problema ya que en nuestros proyectos el receptor de la notificación forma parte de un grupo de profesionales de un marketplace, por lo que se le pide que tenga la aplicación instalada para poder estar dado de alta.

Así que nos decidimos por Telegram. Como no podemos usar un bot, sino que tenemos que enviar notificaciones individuales, necesitamos una API que nos permita controlar una cuenta Telegram desde nuestra aplicación Symfony, para enviar mensajes en su nombre. Para ello disponemos de Telegram messenger CLI, una interfaz de comandos para Telegram.

Telegram messenger CLI

Este interfaz no existe como paquete en Ubuntu, por lo que hemos de compilarlo a mano. Es muy sencillo y rápido. Lo instalaremos en /opt/:

cd /opt/
git clone --recursive https://github.com/vysheng/tg.git
cd tg
sudo apt-get install libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev libjansson-dev libpython-dev make
./configure
make

Ya tenemos instalado «telegram-cli». Ahora hemos de registrar el número de teléfono desde el que se enviarán las notificaciones (previamente hemos de tener Telegram instalado en dicho teléfono, ya que este registro nos enviará un código). Ejecutamos:

$ ./bin/telegram-cli

…que nos pide introducir nuestro número de teléfono (con +34 por delante, por ejemplo +34612345678):

Telegram-cli version 1.4.1, Copyright (C) 2013-2015 Vitaly Valtman
Telegram-cli comes with ABSOLUTELY NO WARRANTY; for details type `show_license'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show_license' for details.
Telegram-cli uses libtgl version 2.1.0
Telegram-cli includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit. (http://www.openssl.org/)
I: config dir=[/home/oper/.config/telegram-cli]
[/home/oper/.telegram-cli] created
[/home/oper/.telegram-cli/downloads] created
phone number: 

Recibiremos un código que introducimos en la consola:

tg-code

… y listo, ya tenemos registrado nuestro número para poder usarlo desde consola.

Daemonizar telegram-cli

De poco nos sirve telegram-cli si solo podemos usarlo desde la consola. La solución para usarlo desde nuestra aplicación Symfony pasa por daemonizar el servicio. Para ello, lanzamos telegram-cli en modo daemon, escuchando en un puerto (puede funcionar mediante sockets de unix, pero al escuchar en un puerto es más fácil debugearlo), y devolviendo sus respuestas en formato JSON. Para simplificar el arranque del servicio, he creado un script en /etc/init.d/telegram-cli que hace todo el trabajo sucio:


#! /bin/sh
# Derived from https://www.domoticz.com/wiki/Installing_Telegram_Notification_System#.2Fetc.2Finit.d.2Ftelegram-cli
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
DESC="Telegram Messaging System"
NAME=telegram-cli
LOGFILE=/var/log/telegramd.log
DAEMON=/opt/tg/bin/telegram-cli
TGPORT=7313
RUNDIR=/var/run/$NAME
SOCKET=$RUNDIR/$NAME.socket

TelegramKeyFile="/opt/tg/server.pub"
DAEMON_ARGS="--json -W -k $TelegramKeyFile -L $LOGFILE -P $TGPORT -d -vvvRC"
PIDFILE=$RUNDIR/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

if [ ! -d "$RUNDIR" ]; then
	mkdir $RUNDIR
fi

# Carry out specific functions when asked to by the system
case "$1" in

    start)
        echo -n "Starting $DESC ... "
        start-stop-daemon --start --background --make-pidfile $PIDFILE --pidfile $PIDFILE \
            --exec $DAEMON -- $DAEMON_ARGS || true
        echo "Done."
    ;;

    stop)
        echo -n "Stopping $DESC ... "
        start-stop-daemon --stop --retry 2 --pidfile $PIDFILE \
            --exec $DAEMON || true
    rm -f $PIDFILE $SOCKET
    echo "Done."

    ;;
    restart)
        echo -n "Restarting $DESC "
        start-stop-daemon --stop --retry 2 --pidfile $PIDFILE \
            --exec $DAEMON || true
        rm -f $PIDFILE $SOCKET
        start-stop-daemon --start --background --make-pidfile $PIDFILE --pidfile $PIDFILE \
            --exec $DAEMON -- $DAEMON_ARGS || true
        echo "Done."
    ;;

    status)
        if [ -f $PIDFILE ]; then
                PID=`cat $PIDFILE`
        else
                echo "No pid file, telegramd not running?"
                exit 1
        fi
        if [ "`ps -p $PID -o comm=`" = "telegram-cli" ]; then
                echo "telegramd running with pid $PID"
        else
                echo "telegramd not running. removing $PIDFILE"
                rm -f $PIDFILE  $SOCKET
                exit 1
        fi
    ;;

    *)
        N=/etc/init.d/$NAME
        echo "Usage $NAME: $SCRIPTNAME {start|stop|restart|status}"
        exit 1
    ;;

esac

Le damos permisos de ejecución y lo configuramos para que arranque en el runlevel 2:

sudo chmod 755 /etc/init.d/telegram-cli
sudo ln -s /etc/init.d/telegram-cli /etc/rc2.d/S99telegram-cli

Ahora, aunque el servidor se reinicie, tendremos nuestro telegram corriendo, y podremos trabajar con él como con cualquier otro daemon:

sudo service telegram-cli start

Incluso podemos conectarnos con nc o telnet y ejecutar cualquier comando:

nc localhost 7313
contact_list

Integración con Symfony

Una vez tenemos telegram-cli escuchando en un puerto, podemos empezar a trabajar con él. Podemos comunicarnos directamente a través de ese puerto, pero es mejor usar una librería como «php-telegram-cli-client«. Se trata de un fork al que le he añadido varias mejoras y solucionado algunos bugs, para que su uso sea todavía más sencillo.

Al tratarse de un fork, composer necesita que le indiquemos el repositorio:

"repositories": [
    {
      "type": "vcs",
      "url": "git@github.com:asilgag/php-telegram-cli-client.git"
    }
  ]

… y también que la versión del repo sea «dev-master»:

"require": {
    "zyberspace/telegram-cli-client": "dev-master"
  }

Y ahora ya podemos enviar nuestras notificaciones. En mi caso, uso el método addAndMsg() que he añadido al fork, para poder enviar mensajes sin necesidad de que el receptor esté en mis contactos (el método comprueba si no está, y lo añade antes de enviarle nada):

$telegram = new \Zyberspace\Telegram\Cli\Client('tcp://localhost:7313');
$telegram->addAndMsg('34612345678', 'Nombre', 'Apellido', 'Hola');

Notificaciones asíncronas

En algunas ocasiones, el envío puede demorarse durante unos segundos, por lo que es conveniente realizarlo en un proceso paralelo que no atasque el hilo principal de ejecución. Me valgo de nohup y popen para ello:

$rootDir = $this->get('kernel')->getRootDir();
$asyncCommand = 'nohup /usr/bin/php '.$rootDir.'/console myproject:push-notification 34612345678 Nombre Apellido Hola &';
popen($asyncCommand, 'r');

Como se ve, ejecuto un comando de mi aplicación en Symfony, «myproject:push-notification». Sólo tengo que preparalo para enviar mis notificaciones:

namespace MyProject\MainBundle\Command;

use Exception;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;


class PushNotificationCommand extends ContainerAwareCommand
{

    /**
     * Configuración inicial
     */
    protected function configure()
    {
        $this
            ->setName('myproject:push-notification')
            ->setDescription('Send push notifications via Telegram')
            ->addArgument('phone', InputArgument::REQUIRED)
            ->addArgument('name', InputArgument::REQUIRED)
            ->addArgument('lastname', InputArgument::REQUIRED)
            ->addArgument('message', InputArgument::REQUIRED);
    }


    /**
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // Obtenemos parámetros
        $phone = $input->getArgument('phone');
        $name = $input->getArgument('name');
        $lastname = $input->getArgument('lastname');
        $message = $input->getArgument('message');

	// Enviamos la notificación
	$telegram = new \Zyberspace\Telegram\Cli\Client('tcp://localhost:7313');
	$telegram->addAndMsg($phone, $name, $lastname, $message);
    }

}

En resumen

Telegram es una muy buena opción para conseguir notificaciones push sencillas desde Symfony. Aparte de ser gratuito, nos permite enviar mensajes personalizados emulando el funcionamiento de un móvil real, y el receptor puede responder directamente al mensaje, iniciando una conversación real con el remitente.

Técnicamente, la instalación del cliente tiene una cierta complejidad, pero una vez conseguido, los envíos se realizan de forma trivial. Ale, ¡a telegramear!