Cómo enviar notificaciones a Slack desde PHP, macOS y Windows

En la actualidad trabajamos con máquinas que realizan procesos largos
o, incluso, esas máquinas están lejos de nosotros. Cuando sucede algún
evento que nos interese, como que ha terminado de procesar un fichero
grande o nuestro sitio web está recibiendo un ataque, recibir una
notificación de este hecho podría ayudarnos a ser más proactivos y vivir
más tranquilos.

Construir un sistema de notificaciones puede ser una tarea enorme e,
incluso, cara económicamente. Pero existen alternativas ya
construidas.

Una forma sencilla de construir nuestro propio sistema de
notificaciones consiste en utilizar Slack y su posibilidad de crear aplicaciones o bots.

La ventaja es que nuestros programas no necesitan conocer cómo
funciona el sistema de notificaciones de Windows, MacOS, Android o iOS.
Solamente tienen que enviar un mensaje a Slack.

¿Qué es Slack?

Slack es una plataforma de comunicación basada principalmente en mensajes organizados mediante espacios de trabajo, canales y conversaciones.

Slack está principalmente orientada a equipos de trabajo pero también
puede resultar útil como herramienta personal. Slack dispone de
aplicaciones para diferentes sistemas operativos y dispositivos móviles,
por lo que un mensaje enviado a nuestro espacio de trabajo puede
convertirse inmediatamente en una notificación en nuestro ordenador,
teléfono móvil o smartWatch, dependiendo de cómo tengamos configuradas
las notificaciones.

Esta característica permite utilizar Slack como intermediario entre
nuestros programas y nuestros dispositivos.

Aprender creando

La mejor forma de aprender algo es haciendo algo práctico. Vamos a
crear la infraestructura necesaria para enviar desde nuestro PC con
Windows, nuestro Macbook con MacOS o nuestro hosting web con PHP un
mensaje a Slack para que nos llegue a todos nuestros dispositivos
conectados a Slack.

Técnicamente haremos esto: nuestro programa detectará un determinado
acontecimiento y realizará una petición HTTPS a la API de Slack. Slack
recibirá el mensaje y se encargará de distribuirlo a nuestros
dispositivos conectados.

Esto permite crear multitud de automatizaciones. Por ejemplo:

  • avisarnos cuando finaliza una copia de seguridad;
  • recibir una alerta cuando se produce un error en nuestra página
    web;
  • conocer cuándo termina una tarea programada;
  • avisarnos si queda poco espacio disponible en un servidor;
  • recibir el resultado de un proceso ejecutado mediante
    cron;
  • saber cuándo termina una conversión de vídeo que tarda varias
    horas;
  • recibir una alerta cuando un determinado servicio deja de
    funcionar;
  • conocer cuándo ha finalizado un proceso en nuestro Mac;
  • o recibir desde Windows el resultado de un script de
    mantenimiento.

Slack gratuito y el
límite de aplicaciones

Para realizar este tutorial podemos utilizar un espacio de trabajo
gratuito de Slack.

Conviene distinguir entre una aplicación y un bot. En este artículo crearemos una aplicación personalizada de Slack y dentro de ella utilizaremos un usuario bot para enviar los mensajes.

En el plan gratuito existen límites sobre las aplicaciones e
integraciones que podemos instalar. Por ejemplo, actualmente Slack
establece un límite de 10 aplicaciones de terceros o
aplicaciones personalizadas instaladas
en un espacio de trabajo gratuito.

Podemos crear una única aplicación, por ejemplo, llamada
Notificaciones y utilizar la misma aplicación para enviar notificaciones desde nuestro MacBook, nuestro PC y nuestro hosting web. Incluso podemos crear diferentes canales y desde el programa elegir dónde publicar.

Con este enfoque una única aplicación de Slack puede centralizar las
notificaciones procedentes de muchos sistemas diferentes.

Crear nuestra aplicación en
Slack

El primer paso consiste en crear una aplicación de Slack.

Debemos acceder a la web de la
API de Slack
y crear una nueva aplicación.

Podemos darle un nombre descriptivo como
Notificaciones.

También tendremos que indicar el espacio de trabajo de Slack en el
que queremos instalarla.

Una aplicación de Slack puede disponer de diferentes permisos. Como
en nuestro caso únicamente queremos publicar mensajes, debemos conceder
al bot el permiso OAuth chat:write. Este permiso permite que la aplicación publique mensajes.

Después instalaremos la aplicación en nuestro espacio de trabajo.
Durante el proceso, Slack generará un Bot User OAuth
Token
.

Los tokens de bot suelen comenzar por xoxb-, por ejemplo:

xoxb-XXXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX

Este token es una credencial secreta.

No debemos publicarlo en una página web, subirlo a un repositorio
público ni incluirlo en un ejemplo que vayamos a compartir con otras
personas.

Cualquier persona que consiga un token válido podría utilizar los
permisos asociados a nuestra aplicación.

Si accidentalmente hacemos público el token, debemos revocarlo y
generar unas credenciales nuevas.

Añadir el bot al canal

También necesitaremos un lugar en el que publicar las
notificaciones.

Podemos crear, por ejemplo, un canal llamado
#notificaciones.

Una vez hecho esto necesitaremos conocer el identificador del
canal.

Los identificadores de canales de Slack tienen un aspecto similar a
C0123456789. Este identificador será el que utilizaremos al llamar a la API.

Slack permite otras formas de organizar y enviar mensajes, pero para
un sistema personal de alertas un canal específico suele ser el enfoque
apropiado.

Cómo se envía un mensaje a
Slack

Slack proporciona el método de su API
chat.postMessage.

La petición se realiza mediante HTTPS utilizando la URL
https://slack.com/api/chat.postMessage.

Debemos enviar nuestro token mediante la cabecera HTTP:

Authorization: Bearer TOKEN

y proporcionar como mínimo el canal y el contenido del mensaje.

Conceptualmente enviaremos un documento JSON parecido a este:

{
    "channel": "C0123456789",
    "text": "Copia de seguridad terminada correctamente"
}

Esta operación es independiente del lenguaje de programación que
utilicemos.

Por esta razón podemos realizar exactamente la misma petición desde
PHP, AppleScript, PowerShell, Python, Java, C# o prácticamente cualquier
entorno capaz de efectuar una petición HTTPS.

Vamos a ver tres ejemplos.

Enviar una
notificación a Slack desde PHP

Supongamos que tenemos una página web alojada en un hosting PHP y
queremos recibir una notificación cuando sucede algún
acontecimiento.

Podría tratarse de una tarea ejecutada mediante cron, una copia de seguridad o un proceso interno de nuestra aplicación.

Una implementación sencilla utilizando cURL sería la siguiente:

<?php

$token = getenv('SLACK_BOT_TOKEN');
$canal = 'C0123456789';

$datos = [
    'channel' => $canal,
    'text' => 'Mensaje enviado desde mi servidor PHP'
];

$curl = curl_init('https://slack.com/api/chat.postMessage');

curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json; charset=utf-8'
    ],
    CURLOPT_POSTFIELDS => json_encode($datos)
]);

$respuesta = curl_exec($curl);

curl_close($curl);

echo $respuesta;

El token se obtiene mediante:

getenv('SLACK_BOT_TOKEN')

en lugar de escribirlo directamente en el código.

Siempre que nuestro hosting lo permita, resulta preferible almacenar
credenciales y secretos mediante variables de entorno o mediante algún
mecanismo de configuración situado fuera del directorio público de
nuestra página web.

Crear una función
PHP para enviar mensajes

Si vamos a utilizar Slack desde diferentes partes de nuestra
aplicación PHP, podemos encapsular el código anterior en una
función:

function enviarSlack($mensaje)
{
    $token = getenv('SLACK_BOT_TOKEN');
    $canal = 'C0123456789';

    $datos = [
        'channel' => $canal,
        'text' => $mensaje
    ];

    $curl = curl_init('https://slack.com/api/chat.postMessage');

    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json; charset=utf-8'
        ],
        CURLOPT_POSTFIELDS => json_encode($datos)
    ]);

    $respuesta = curl_exec($curl);

    curl_close($curl);

    return $respuesta;
}

Ahora podemos enviar una notificación simplemente escribiendo:

enviarSlack('La copia de seguridad ha terminado correctamente.');

Para enviar una notificación de que se ha terminado la copia de
seguridad sería algo como:

if ($copiaCorrecta) {
    enviarSlack('✅ Copia de seguridad terminada correctamente.');
} else {
    enviarSlack('⚠️ Se ha producido un error durante la copia de seguridad.');
}

Podemos incluir en el mensaje cualquier información que resulte
útil:

enviarSlack(
    "Nuevo pedido recibido.\n" .
    "Número: " . $pedido . "\n" .
    "Importe: " . $importe . " euros"
);

De esta manera, un acontecimiento producido en nuestro hosting puede
generar inmediatamente una notificación en nuestro teléfono.

Enviar una
notificación desde un Mac con AppleScript

El mismo sistema puede utilizarse desde macOS.

AppleScript no necesita disponer de una implementación específica de
la API de Slack. Podemos utilizar el comando curl incluido en macOS mediante do shell script.

Un ejemplo sería:

set slackToken to "xoxb-XXXXXXXXXXXX"
set slackChannel to "C0123456789"
set slackMessage to "El proceso del Mac ha terminado correctamente"

set jsonData to "{\"channel\":\"" & slackChannel & "\",\"text\":\"" & slackMessage & "\"}"

set command to "curl -s -X POST " & ¬
    "-H " & quoted form of ("Authorization: Bearer " & slackToken) & " " & ¬
    "-H " & quoted form of "Content-Type: application/json; charset=utf-8" & " " & ¬
    "--data " & quoted form of jsonData & " " & ¬
    quoted form of "https://slack.com/api/chat.postMessage"

do shell script command

En un script real debemos evitar almacenar el token directamente en
el código cuando exista la posibilidad de que ese archivo pueda ser
compartido.

Una posibilidad es guardarlo en una variable de entorno, un archivo
de configuración protegido o utilizar el llavero de macOS.

Avisarnos cuando
termina un proceso en el Mac

Supongamos que utilizamos nuestro Mac para ejecutar una tarea que
tarda mucho tiempo.

Nuestro AppleScript podría realizar primero el proceso:

do shell script "/ruta/a/mi/programa"

y después enviar el mensaje a Slack:

set slackMessage to "El proceso ha terminado."

De esta manera podemos abandonar el ordenador y recibir una
notificación en el teléfono cuando la tarea haya finalizado.

También podríamos utilizar el resultado del proceso para enviar
mensajes diferentes dependiendo de si la operación ha terminado
correctamente o se ha producido algún error.

Enviar
una notificación desde Windows con un archivo BAT

Windows también puede participar en nuestro sistema de
notificaciones.

Podemos utilizar PowerShell para realizar la petición HTTP e
invocarlo desde un archivo .bat.

Por ejemplo, podemos crear un archivo llamado
notificar.bat con el siguiente contenido:

@echo off

set "SLACK_TOKEN=xoxb-XXXXXXXXXXXX"
set "SLACK_CHANNEL=C0123456789"
set "SLACK_MESSAGE=El proceso de Windows ha terminado"

powershell -NoProfile -Command ^
  "$headers = @{ Authorization = 'Bearer %SLACK_TOKEN%' }; ^
   $body = @{ channel = '%SLACK_CHANNEL%'; text = '%SLACK_MESSAGE%' } | ConvertTo-Json; ^
   Invoke-RestMethod -Uri 'https://slack.com/api/chat.postMessage' -Method Post -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $body"
Ejecutar la
notificación después de otro programa

Una utilidad interesante de los archivos BAT consiste en encadenar
varios comandos.

Por ejemplo:

@echo off

backup.exe

if %ERRORLEVEL% EQU 0 (
    call notificar.bat
)

En este ejemplo se ejecuta primero:

backup.exe

y se consulta posteriormente su código de salida.

Podemos desarrollar el ejemplo para generar una notificación
diferente cuando se produzca un error:

@echo off

backup.exe

if %ERRORLEVEL% EQU 0 (
    set "RESULTADO=La copia de seguridad ha terminado correctamente"
) else (
    set "RESULTADO=ERROR: la copia de seguridad ha fallado"
)

A continuación podemos utilizar el valor de RESULTADO como contenido del mensaje enviado a Slack.

El principio es exactamente el mismo que en PHP y AppleScript:
nuestro programa detecta un acontecimiento y realiza una petición a la
API de Slack.

Comprobar la respuesta de
Slack

Hay un detalle importante que no debemos ignorar en una
implementación real.

No basta con comprobar que la conexión HTTPS se ha realizado
correctamente.

La API de Slack devuelve un documento JSON indicando si la operación
ha tenido éxito.

Una respuesta correcta contendrá un valor equivalente a:

{
    "ok": true
}

Mientras que, si existe algún problema, recibiremos una respuesta
similar a:

{
    "ok": false,
    "error": "..."
}

Por tanto, nuestros scripts deberían comprobar el campo
ok cuando sea importante asegurarnos de que la notificación ha sido aceptada por Slack.

Esto permite detectar situaciones como un token incorrecto, permisos
insuficientes o un canal al que la aplicación no puede acceder.

Seguridad

El elemento más delicado de todo este sistema es el token OAuth del
bot.

Debemos tratarlo igual que una contraseña.

En particular, conviene seguir varias reglas:

  • no publicarlo en GitHub ni en otros repositorios públicos;
  • no incluirlo en artículos, capturas de pantalla o
    documentación;
  • evitar almacenarlo directamente dentro del código;
  • impedir que pueda descargarse desde nuestro servidor web;
  • utilizar variables de entorno o mecanismos seguros de almacenamiento
    de credenciales;
  • revocarlo inmediatamente si sospechamos que ha quedado
    expuesto.

También es aconsejable conceder a la aplicación únicamente los
permisos que necesita.

Para los ejemplos de este tutorial nos interesa utilizar
chat:write.

No existe ninguna razón para conceder acceso a mensajes, usuarios,
archivos u otros recursos si nuestra aplicación solamente necesita
publicar una notificación.

Aplicar el principio del mínimo privilegio reduce las consecuencias
de una posible filtración del token.

No utilizar Slack
como sistema de registro

Aunque podemos enviar mensajes automáticos, Slack no debería
convertirse en el registro completo de actividad de nuestro
servidor.

No sería razonable, por ejemplo, enviar una notificación por cada
petición HTTP recibida por una página web.

Las notificaciones deben reservarse para acontecimientos que
realmente queremos conocer.

El envío masivo puede encontrarse con los límites de frecuencia
establecidos por Slack.

Para almacenar grandes cantidades de acontecimientos debemos utilizar
archivos de registro, bases de datos o plataformas específicas de
monitorización. Slack puede actuar como la última etapa del sistema y
avisarnos únicamente cuando ocurre algo que requiere nuestra
atención.

Limitaciones de la cuenta
gratuita

El plan gratuito de Slack es suficiente para un sistema doméstico o
un pequeño sistema de notificaciones como el descrito en este tutorial,
pero debemos tener presentes sus restricciones.

Slack también establece limitaciones sobre el historial disponible en
los espacios de trabajo gratuitos y aplica límites de frecuencia a las
llamadas realizadas contra su API.

Estas restricciones no suelen representar un problema para un sistema
que envía unas pocas alertas al día, pero son importantes si pretendemos
utilizar Slack para registrar automáticamente una gran cantidad de
acontecimientos.

Eliminar todas las descargas de iCloud drive en MacOS

El servicio de almacenamiento en la nube incluido en los dispositivos de Apple, más conocido como iCloud drive, ofrece una completa integración en todo el entorno Apple. Incluso permite el acceso desde la Web.

Cuando se activa iCloud Drive en macOS, el sistema puede conservar una copia local de los archivos para un acceso más rápido, o dejar solo un enlace a ese archivo y mantener el contenido real en la nube para ahorrar espacio.
En ocasiones podemos querer forzar que macOS elimine las copias descargadas en el disco sin borrar nada de iCloud para recuperar espacio en nuestro Mac.

Podemos abrir la carpeta de iCloud en el Finder e ir uno a uno por cada archivo, abrir el menú contextual y activar la opción eliminar descarga. Pero esta tarea se puede complicar si tenemos multitud de carpetas y sub carpetas y miles de archivos almacenados en iCloud.

La Terminal al rescate

Desde la Terminal de MacOS podemos realizar operaciones complejas que no tienen forma de ser realizadas desde la interfaz gráfica de MacOS.

Si abrimos la Terminal de MacOS y ejecutamos el siguiente comando se realizará esa limpieza de todos los archivos de iCloud de nuestro equipo:

brctl evict ~/Library/Mobile\ Documents/com~apple~CloudDocs

Comando brctl

brctl (abreviatura de bird control) es una utilidad incluida en macOS que permite interactuar con los servicios de iCloud a bajo nivel.

El subcomando evict solicita al servicio que “evacúe” (evict) las copias locales de los archivos indicados: el archivo local se sustituye por un marcador que representa el ítem en iCloud, y el contenido real queda solo en la nube.

Desde el punto de vista del usuario, el archivo sigue apareciendo en Finder, pero macOS no mantiene su contenido en disco; si se abre de nuevo, el sistema lo descargará cuando sea necesario.

Aún más fácil con AppleScript

Puede que abrir la Terminal no sea del agrado de todos los usuarios por su aparente complejidad. Para evitar esto podemos crear un AppleScript con el siguiente contenido:

on run
    set icloudPath to POSIX path of (path to library folder from user domain) & "Mobile Documents/com~apple~CloudDocs"
    set command to "brctl evict " & quoted form of icloudPath
    do shell script command
end run

Ejecutando este AppleScript evitaremos usar la Terminal. Además, lo podemos convertir en una aplicación para crear automatizaciones.

Obtener el idioma del sistema en AppleScript

En algunos proyectos con AppleScript puede que queramos dar soporte a varios idiomas. La solución inicial pasa por distribuir nuestro fichero de AppleScript adaptado a cada uno de los idiomas que queramos incluir en el proyecto. Esta solución es poco eficiente e incrementa mucho el coste de mantenimiento.

Pero si nuestro AppleScript pudiese detectar qué idioma necesita el usuario y se pudiera adaptar la selección de cadenas de texto al idioma que necesitemos podríamos utilizar un único fichero de AppleScript para todos los idioma.

Limitaciones y soluciones para identificar el idioma

En principio AppleScript no puede acceder a la información del sistema desde su propia librería de comandos. Pero sí podemos usar otros entornos para acceder a dicha información.

Por ejemplo, con el comando defaults tenemos acceso a mucha de la información del sistema. Si ejecutamos en la Terminal de MacOS el comando defaults read NSGlobalDomain AppleLanguages por la pantalla de la Terminal aparecerá algo como:

(
"es-ES",
"en-ES"
)

Es un array o lista de los idiomas y regiones que tenemos instalado en nuestro sistema. Como buscamos obtener un resultado con el texto es que representa a Español debemos cortar todo el texto sobrante para quedarnos con ese primer elemento de la lista.

Con el comando sed podemos realizar todas estas operaciones. Enlazando el comando defaults con sed podemos obtener lo que buscamos utilizando el comando:

defaults read NSGlobalDomain AppleLanguages | sed -n '2s/[^a-zA-Z-]//gp' | cut -d '-' -f1 

Al ejecutarlo en la Terminal aparecerá:

es

Ahora podemos utilizar un comando de Terminal en un AppleScript.

Función para AppleScript

Nuestra función finalmente quedaría así:

on getSystemLanguage()
    return do shell script "defaults read NSGlobalDomain AppleLanguages | sed -n '2s/[^a-zA-Z-]//gp' | cut -d '-' -f1"
end getSystemLanguage

Actualizado el script de VoiceOverTranslator

El script de AppleScript de VoiceOverTranslator ha sido actualizado incluyendo un script extra que nos permite traducir el contenido que tengamos almacenado en el portapapeles.

Ahora podemos selecciona un bloque de texto completo y utilizando las funciones de la API de GoogleTrans obtener una traducción a nuestro idioma preferido.

Puedes obtener estos dos scripts en el repositorio de Tyflos Accessible Software para VoiceOverTranslator.

Obtener el contenido del portapapeles con AppleScript

Ya hemos visto que en AppleScript podemos acceder a mucha de la información que está disponible en nuestro equipo pero aún no hemos visto una de las funciones más simples y útiles de los sistemas operativos modernos: el portapapeles.

El portapapeles es un espacio en memoria donde se aloja una copia del contenido que hayamos copiado o cortado con las funciones del sistema operativo.

¿Para qué podemos querer acceder a la información de portapapeles desde un script? Pues desde una simple función para mostrar el contenido del portapapeles en un cuadro de diálogo para verificar qué tenemos almacenado en esa memoria o para crear un script que nos traduzca el texto del portapapeles al idioma que necesitemos.

Con AppleScript podemos acceder a ese espacio de memoria utilizando la variable the clipboard. Esta variable de AppleScript puede almacenar cualquier tipo de contenido como imágenes, texto, audio o la información de un fichero de una carpeta. Esto nos obliga a convertir esta información en algo comprensible para nuestro script.

Veamos un ejemplo en el que obtenemos como texto la información guardada en el portapapeles:

set clipboardContent to (the clipboard as text)

Con esto podemos crear un script para verbalizar el contenido del portapapeles utilizando la voz del sistema. El código sería algo como esto:

set clipboardContent to the clipboard as text

say clipboardContent

Este código funciona pero no tiene en cuenta si en el portapapeles el contenido que está almacenado es convertible a texto. Para evitar posibles errores de ejecución de nuestro script debemos emplear un bloque try que nos permita controlar errores de ejecución en un script. El código quedaría así:

try

  set clipboardContent to the clipboard as text

  say clipboardContent

on error

  say "El portapapeles no contiene texto"

end try

Con esta modificación nuestro script siempre verbalizará algo aunque sea para notificarnos que el contenido de nuestro portapapeles no es texto.

Disponible remapeador de teclas para MacOS

Los teclados físicos disponibles en los equipos Macbook y Macbook pro no incluyen ciertas teclas que si posee un teclado expandido. En algunas ocasiones, como por ejemplo al acceder a una máquina virtual o utilizar un software técnico, puede que necesitemos la tecla control derecha o que la tecla comando izquierda se comporte de otra manera. La solución pasa por cambiar el mapa de valores de teclado de nuestro Mac.

En el artículo Cambia la tecla BlockMayus por otra en MacOS de forma rápida compartí una solución de un AppleScript para realizar un cambio específico para la tecla BlockMayus.

Ahora en el mismo repositorio de ChangeKeyboardForVM está disponible un segundo script llamado KeyboardRemapper que permite seleccionar la tecla a cambiar y por qué tecla se quiere cambiar ampliando las posibilidades de personalización de nuestro Macbook.