Los bots de Slack no tienen que esperar a que escribas comandos. Con la configuración adecuada, tu bot puede ayudarte a gestionar tus sitios de WordPress ofreciéndote botones interactivos, desplegables, tareas programadas y alertas inteligentes, todo ello dentro de Slack.
En este artículo, te mostraremos cómo añadir interactividad, automatización y monitorización a tu bot de Slack.
Requisitos previos
Antes de empezar, asegúrate de que tienes:
- Una aplicación Slack con permisos de bot y un comando slash.
- Una cuenta de Kinsta con acceso a la API y un sitio con el que hacer pruebas.
- Node.js y NPM instalados localmente.
- Familiaridad básica con JavaScript (o al menos estar cómodo copiando y retocando código).
- Claves API para Slack y Kinsta.
Cómo empezar
Para crear este Slackbot, se utilizan Node.js y el framework Bolt de Slack para conectar los comandos slash que activan acciones a través de la API de Kinsta.
No vamos a repetir todos los pasos para crear una aplicación Slack u obtener acceso a la API de Kinsta en esta guía, ya que eso ya lo hemos explicado en nuestra guía anterior, Cómo Construir un Slackbot con Node.js y la API de Kinsta para la Gestión de Sitios.
Si aún no la has visto, léela primero. Te guía a través de la creación de tu aplicación Slack, la obtención de tu token bot y tu secreto de firma, y la obtención de tu clave API de Kinsta.
Añade interactividad a tu Slackbot
Los Slackbots no tienen por qué depender sólo de comandos slash. Con componentes interactivos como botones, menús y modales, puedes convertir tu bot en una herramienta mucho más intuitiva y fácil de usar.
En lugar de escribir /clear_cache environment_id
, imagina hacer clic en un botón llamado Borrar caché justo después de comprobar el estado de un sitio. Para ello, necesitas el cliente API Web de Slack. Instálalo en tu proyecto con el siguiente comando:
npm install @slack/web-api
A continuación, inícialo en tu app.js
:
const { WebClient } = require('@slack/web-api');
const web = new WebClient(process.env.SLACK_BOT_TOKEN);
Asegúrate de que SLACK_BOT_TOKEN
está configurado en tu archivo .env
. Ahora, vamos a mejorar el comando /site_status
del artículo anterior. En lugar de limitarnos a enviar texto, adjuntaremos botones para acciones rápidas como Borrar caché, Crear copia de seguridad o Comprobar estado detallado.
Este es el aspecto que tiene el controlador actualizado:
app.command('/site_status', async ({ command, ack, say }) => {
await ack();
const environmentId = command.text.trim();
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/site_status [environment-id]`');
return;
}
try {
// Get environment status
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (response && response.site && response.site.environments && response.site.environments.length > 0) {
const env = response.site.environments[0];
// Format the status message
let statusMessage = formatSiteStatus(env);
// Send message with interactive buttons
await web.chat.postMessage({
channel: command.channel_id,
text: statusMessage,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: statusMessage
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: '🧹 Clear Cache',
emoji: true
},
value: environmentId,
action_id: 'clear_cache_button'
},
{
type: 'button',
text: {
type: 'plain_text',
text: '📊 Detailed Status',
emoji: true
},
value: environmentId,
action_id: 'detailed_status_button'
},
{
type: 'button',
text: {
type: 'plain_text',
text: '💾 Create Backup',
emoji: true
},
value: environmentId,
action_id: 'create_backup_button'
}
]
}
]
});
} else {
await say(`⚠️ No environment found with ID: `${environmentId}``);
}
} catch (error) {
console.error('Error checking site status:', error);
await say(`❌ Error checking site status: ${error.message}`);
}
});
Cada clic en un botón desencadena una acción. Así es como gestionamos el botón Borrar caché:
// Add action handlers for the buttons
app.action('clear_cache_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
await respond(`🔄 Clearing cache for environment `${environmentId}`...`);
try {
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
await respond(`✅ Cache clearing operation started! Operation ID: `${response.operation_id}``);
} else {
await respond('⚠️ Cache clearing request was sent, but no operation ID was returned.');
}
} catch (error) {
console.error('Cache clearing error:', error);
await respond(`❌ Error clearing cache: ${error.message}`);
}
});
Puedes seguir el mismo patrón para los botones de copia de seguridad y de estado, simplemente vinculando cada uno al endpoint de la API o a la lógica de comandos adecuados.
// Handlers for other buttons
app.action('detailed_status_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
// Implement detailed status check similar to the /detailed_status command
// ...
});
app.action('create_backup_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
// Implement backup creation similar to the /create_backup command
// ...
});
Utiliza un desplegable para seleccionar un sitio
Escribir los ID de los entornos no es precisamente divertido. ¿Y esperar que todos los miembros del equipo recuerden qué ID corresponde a cada entorno? Eso no es realista.
Vamos a hacerlo más intuitivo. En lugar de pedir a los usuarios que escriban /site_status [environment-id]
, les daremos un desplegable de Slack en el que podrán elegir un sitio de una lista. Una vez que seleccionen uno, el bot mostrará el estado y adjuntará los mismos botones de acción rápida que implementamos antes.
Para ello:
- Obtenemos todos los sitios de la API de Kinsta
- Obtenemos los entornos de cada sitio
- Construimos un menú desplegable con estas opciones
- Gestionamos la selección del usuario y mostramos el estado del sitio
Este es el comando que muestra el menú desplegable:
app.command('/select_site', async ({ command, ack, say }) => {
await ack();
try {
// Get all sites
const response = await kinstaRequest('/sites');
if (response && response.company && response.company.sites) {
const sites = response.company.sites;
// Create options for each site
const options = [];
for (const site of sites) {
// Get environments for this site
const envResponse = await kinstaRequest(`/sites/${site.id}/environments`);
if (envResponse && envResponse.site && envResponse.site.environments) {
for (const env of envResponse.site.environments) {
options.push({
text: {
type: 'plain_text',
text: `${site.name} (${env.name})`
},
value: env.id
});
}
}
}
// Send message with dropdown
await web.chat.postMessage({
channel: command.channel_id,
text: 'Select a site to manage:',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Select a site to manage:*'
},
accessory: {
type: 'static_select',
placeholder: {
type: 'plain_text',
text: 'Select a site'
},
options: options.slice(0, 100), // Slack has a limit of 100 options
action_id: 'site_selected'
}
}
]
});
} else {
await say('❌ Error retrieving sites. Please check your API credentials.');
}
} catch (error) {
console.error('Error:', error);
await say(`❌ Error retrieving sites: ${error.message}`);
}
});
Cuando un usuario elige un sitio, lo gestionamos con este controlador de acción:
// Handle the site selection
app.action('site_selected', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].selected_option.value;
const siteName = body.actions[0].selected_option.text.text;
// Get environment status
try {
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (response && response.site && response.site.environments && response.site.environments.length > 0) {
const env = response.site.environments[0];
// Format the status message
let statusMessage = `*${siteName}* (ID: `${environmentId}`)nn${formatSiteStatus(env)}`;
// Send message with interactive buttons (similar to the site_status command)
// ...
} else {
await respond(`⚠️ No environment found with ID: `${environmentId}``);
}
} catch (error) {
console.error('Error:', error);
await respond(`❌ Error retrieving environment: ${error.message}`);
}
});
Ahora que nuestro bot puede desencadenar acciones con un botón y seleccionar sitios de una lista, asegurémonos de no ejecutar accidentalmente operaciones arriesgadas.
Diálogos de confirmación
Algunas operaciones nunca deben ejecutarse accidentalmente. Borrar el caché puede parecer inofensivo, pero si estás trabajando en un sitio en producción, probablemente no quieras hacerlo con un solo clic — especialmente si solo estabas comprobando el estado del sitio. Ahí es donde entran en juego los modales (cuadros de diálogo) de Slack.
En lugar de borrar inmediatamente el caché cuando se hace clic en clear_cache_button
, mostramos un modal de confirmación. Aquí te explicamos cómo:
app.action('clear_cache_button', async ({ body, ack, context }) => {
await ack();
const environmentId = body.actions[0].value;
// Open a confirmation dialog
try {
await web.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
callback_id: 'clear_cache_confirmation',
private_metadata: environmentId,
title: {
type: 'plain_text',
text: 'Confirm Cache Clearing'
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Are you sure you want to clear the cache for environment `${environmentId}`?`
}
}
],
submit: {
type: 'plain_text',
text: 'Clear Cache'
},
close: {
type: 'plain_text',
text: 'Cancel'
}
}
});
} catch (error) {
console.error('Error opening confirmation dialog:', error);
}
});
En el código anterior, utilizamos web.views.open()
para lanzar un modal con un título claro, un mensaje de advertencia y dos botones — Borrar Caché y Cancelar — y almacenamos el environmentId
en private_metadata
para tenerlo cuando el usuario haga clic en Borrar Caché.
Una vez que el usuario hace clic en el botón Borrar Caché del modal, Slack envía un evento view_submission
. A continuación te explicamos cómo gestionarlo y proceder con la operación:
// Handle the confirmation dialog submission
app.view('clear_cache_confirmation', async ({ ack, body, view }) => {
await ack();
const environmentId = view.private_metadata;
const userId = body.user.id;
// Find a DM channel with the user to respond to
const result = await web.conversations.open({
users: userId
});
const channel = result.channel.id;
await web.chat.postMessage({
channel,
text: `🔄 Clearing cache for environment `${environmentId}`...`
});
try {
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
await web.chat.postMessage({
channel,
text: `✅ Cache clearing operation started! Operation ID: `${response.operation_id}``
});
} else {
await web.chat.postMessage({
channel,
text: '⚠️ Cache clearing request was sent, but no operation ID was returned.'
});
}
} catch (error) {
console.error('Cache clearing error:', error);
await web.chat.postMessage({
channel,
text: `❌ Error clearing cache: ${error.message}`
});
}
});
En este código, después de que el usuario confirme, tomamos el environmentId
de private_metadata
, abrimos un DM privado utilizando web.conversations.open()
para evitar saturar los canales públicos, ejecutamos la solicitud de la API para borrar el caché, y seguimos con un mensaje de éxito o error dependiendo del resultado.
Indicadores de progreso
Algunos comandos de Slack son instantáneos, como borrar un caché o comprobar un estado. Pero otros… no tanto.
Crear una copia de seguridad o desplegar archivos puede llevar varios segundos o incluso minutos. Y si tu bot permanece en silencio durante ese tiempo, los usuarios podrían asumir que algo se ha roto.
Slack no te ofrece una barra de progreso integrada, pero podemos simular una con un poco de creatividad. Aquí tienes una función auxiliar que actualiza un mensaje con una barra de progreso visual utilizando el kit de bloques:
async function updateProgress(channel, messageTs, text, percentage) {
// Create a progress bar
const barLength = 20;
const filledLength = Math.round(barLength * (percentage / 100));
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
await web.chat.update({
channel,
ts: messageTs,
text: `${text} [${percentage}%]`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${text} [${percentage}%]n`${bar}``
}
}
]
});
}
Integremos esto en un comando /create_backup
. En lugar de esperar a que se complete toda la operación antes de responder, comprobaremos el progreso del usuario en cada paso.
app.command('/create_backup', async ({ command, ack, say }) => {
await ack();
const args = command.text.split(' ');
const environmentId = args[0];
const tag = args.length > 1 ? args.slice(1).join(' ') : `Manual backup ${new Date().toISOString()}`;
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/create_backup [environment-id] [optional-tag]`');
return;
}
// Post initial message and get its timestamp for updates
const initial = await say('🔄 Initiating backup...');
const messageTs = initial.ts;
try {
// Update progress to 10%
await updateProgress(command.channel_id, messageTs, '🔄 Creating backup...', 10);
// Call Kinsta API to create a backup
const response = await kinstaRequest(
`/sites/environments/${environmentId}/manual-backups`,
'POST',
{ tag }
);
if (response && response.operation_id) {
await updateProgress(command.channel_id, messageTs, '🔄 Backup in progress...', 30);
// Poll the operation status
let completed = false;
let percentage = 30;
while (!completed && percentage setTimeout(resolve, 3000));
// Check operation status
const statusResponse = await kinstaRequest(`/operations/${response.operation_id}`);
if (statusResponse && statusResponse.operation) {
const operation = statusResponse.operation;
if (operation.status === 'completed') {
completed = true;
percentage = 100;
} else if (operation.status === 'failed') {
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: `❌ Backup failed! Error: ${operation.error || 'Unknown error'}`
});
return;
} else {
// Increment progress
percentage += 10;
if (percentage > 95) percentage = 95;
await updateProgress(
command.channel_id,
messageTs,
'🔄 Backup in progress...',
percentage
);
}
}
}
// Final update
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: `✅ Backup completed successfully!`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `✅ Backup completed successfully!n*Tag:* ${tag}n*Operation ID:* `${response.operation_id}``
}
}
]
});
} else {
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: '⚠️ Backup request was sent, but no operation ID was returned.'
});
}
} catch (error) {
console.error('Backup creation error:', error);
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: `❌ Error creating backup: ${error.message}`
});
}
});
Notificaciones de éxito/fracaso
En este momento, tu bot probablemente te envía texto plano como ✅ Éxito o ❌ Error. Funciona, pero es muy básico y no ayuda a los usuarios a comprender por qué algo ha tenido éxito o qué deben hacer si falla.
Vamos a solucionarlo con un formato adecuado para los mensajes de éxito y error, junto con contexto útil, sugerencias y un formato limpio.
Añade estas utilidades a tu utils.js
para poder reutilizarlas en todos los comandos:
function formatSuccessMessage(title, details = []) {
let message = `✅ *${title}*nn`;
if (details.length > 0) {
details.forEach(detail => {
message += `• ${detail.label}: ${detail.value}n`;
});
}
return message;
}
function formatErrorMessage(title, error, suggestions = []) {
let message = `❌ *${title}*nn`;
message += `*Error:* ${error}nn`;
if (suggestions.length > 0) {
message += '*Suggestions:*n';
suggestions.forEach(suggestion => {
message += `• ${suggestion}n`;
});
}
return message;
}
module.exports = {
connectToSite,
logCommand,
formatSuccessMessage,
formatErrorMessage
};
Estas funciones toman entradas estructuradas y las convierten en markdown compatible con Slack con emojis, etiquetas y saltos de línea. Mucho más fácil de leer en medio de un hilo de Slack muy activo. Así es como se ve dentro de un controlador de comandos real. Vamos a usar /clear_cache
como ejemplo:
app.command('/clear_cache', async ({ command, ack, say }) => {
await ack();
const environmentId = command.text.trim();
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/clear_cache [environment-id]`');
return;
}
try {
await say('🔄 Processing...');
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
const { formatSuccessMessage } = require('./utils');
await say(formatSuccessMessage('Cache Clearing Started', [
{ label: 'Environment ID', value: ``${environmentId}`` },
{ label: 'Operation ID', value: ``${response.operation_id}`` },
{ label: 'Status', value: 'In Progress' }
]));
} else {
const { formatErrorMessage } = require('./utils');
await say(formatErrorMessage(
'Cache Clearing Error',
'No operation ID returned',
[
'Check your environment ID',
'Verify your API credentials',
'Try again later'
]
));
}
} catch (error) {
console.error('Cache clearing error:', error);
const { formatErrorMessage } = require('./utils');
await say(formatErrorMessage(
'Cache Clearing Error',
error.message,
[
'Check your environment ID',
'Verify your API credentials',
'Try again later'
]
));
}
});
Automatizar tareas de WordPress con tareas programadas
Hasta ahora, todo lo que hace tu Slackbot ocurre cuando alguien activa explícitamente un comando. Pero no todo debe depender de que alguien se acuerde de ejecutarlo.
¿Y si tu bot pudiera hacer copias de seguridad automáticas de tus sitios cada noche? O comprobar si algún sitio está caído cada mañana antes de que el equipo se despierte.
Utilizaremos la biblioteca node-schedule para ejecutar tareas basadas en expresiones cron. Primero, instálala:
npm install node-schedule
Ahora, instálala en la parte superior de tu app.js
:
const schedule = require('node-schedule');
También necesitaremos una forma de hacer un seguimiento de las tareas programadas activas para que los usuarios puedan listarlas o cancelarlas más tarde:
const scheduledJobs = {};
Crear el comando schedule task (programar tarea)
Empezaremos con un comando básico /schedule_task
que acepta un tipo de tarea (backup
, clear_cache
, o status_check
), el ID del entorno y una expresión cron.
/schedule_task backup 12345 0 0 * * *
Esto programaría una copia de seguridad diaria a medianoche. Aquí tienes el controlador de comandos completo:
app.command('/schedule_task', async ({ command, ack, say }) => {
await ack();
const args = command.text.split(' ');
if (args.length {
console.log(`Running scheduled ${taskType} for environment ${environmentId}`);
try {
switch (taskType) {
case 'backup':
await kinstaRequest(`/sites/environments/${environmentId}/manual-backups`, 'POST', {
tag: `Scheduled backup ${new Date().toISOString()}`
});
break;
case 'clear_cache':
await kinstaRequest(`/sites/environments/${environmentId}/clear-cache`, 'POST');
break;
case 'status_check':
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
const env = response?.site?.environments?.[0];
if (env) {
console.log(`Status: ${env.display_name} is ${env.is_blocked ? 'blocked' : 'running'}`);
}
break;
}
} catch (err) {
console.error(`Scheduled ${taskType} failed for ${environmentId}:`, err.message);
}
});
scheduledJobs[jobId] = {
job,
taskType,
environmentId,
cronSchedule,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(`✅ Scheduled task created!
*Task:* ${taskType}
*Environment:* `${environmentId}`
*Cron:* `${cronSchedule}`
*Job ID:* `${jobId}`
To cancel this task, run `/cancel_task ${jobId}``);
} catch (err) {
console.error('Error creating scheduled job:', err);
await say(`❌ Failed to create scheduled task: ${err.message}`);
}
});
Cancelación de tareas programadas
Si algo cambia o la tarea ya no es necesaria, los usuarios pueden cancelarla con:
/cancel_task
Esta es la implementación:
app.command('/cancel_task', async ({ command, ack, say }) => {
await ack();
const jobId = command.text.trim();
if (!scheduledJobs[jobId]) {
await say(`⚠️ No task found with ID: `${jobId}``);
return;
}
scheduledJobs[jobId].job.cancel();
delete scheduledJobs[jobId];
await say(`✅ Task `${jobId}` has been cancelled.`);
});
Listado de todas las tareas programadas
Vamos a permitir también que los usuarios vean todos las tareas que se han programado:
app.command('/list_tasks', async ({ command, ack, say }) => {
await ack();
const tasks = Object.entries(scheduledJobs);
if (tasks.length === 0) {
await say('No scheduled tasks found.');
return;
}
let message = '*Scheduled Tasks:*nn';
for (const [jobId, job] of tasks) {
message += `• *Job ID:* `${jobId}`n`;
message += ` - Task: ${job.taskType}n`;
message += ` - Environment: `${job.environmentId}`n`;
message += ` - Cron: `${job.cronSchedule}`n`;
message += ` - Created by: nn`;
}
message += '_Use `/cancel_task [job_id]` to cancel a task._';
await say(message);
});
Esto da a tu Slackbot un nuevo nivel de autonomía. Las copias de seguridad, las limpiezas de caché y las comprobaciones de estado ya no tienen que ser trabajo de alguien. Simplemente se realizan de forma silenciosa, fiable y programada.
Mantenimiento recurrente
Algunas veces, puede que quieras ejecutar un grupo de tareas de mantenimiento a intervalos regulares, como copias de seguridad semanales y limpieza del caché los domingos por la noche. Ahí es donde entran en juego las ventanas de mantenimiento.
Una ventana de mantenimiento es un bloque de tiempo programado en el que el bot ejecuta automáticamente tareas predefinidas como:
- Crear una copia de seguridad
- Limpiar el caché
- Enviar notificaciones de inicio y finalización
El formato es sencillo:
/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]
Por ejemplo:
/maintenance_window 12345 Sunday 2 3
Esto significa que todos los domingos a las 2 de la madrugada se ejecutan tareas de mantenimiento durante 3 horas. Aquí tienes la implementación completa:
// Add a command to create a maintenance window
app.command('/maintenance_window', async ({ command, ack, say }) => {
await ack();
// Expected format: environment_id day_of_week hour duration
// Example: /maintenance_window 12345 Sunday 2 3
const args = command.text.split(' ');
if (args.length < 4) {
await say('Please provide all required parameters. Usage: `/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]`');
return;
}
const [environmentId, dayOfWeek, hour, duration] = args;
// Validate inputs
const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
if (!validDays.includes(dayOfWeek)) {
await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
return;
}
const hourInt = parseInt(hour, 10);
if (isNaN(hourInt) || hourInt 23) {
await say('Hour must be a number between 0 and 23.');
return;
}
const durationInt = parseInt(duration, 10);
if (isNaN(durationInt) || durationInt 12) {
await say('Duration must be a number between 1 and 12 hours.');
return;
}
// Convert day of week to cron format
const dayMap = {
'Sunday': 0,
'Monday': 1,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 4,
'Friday': 5,
'Saturday': 6
};
const cronDay = dayMap[dayOfWeek];
// Create cron schedule for the start of the maintenance window
const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
// Generate a unique job ID
const jobId = `maintenance_${environmentId}_${Date.now()}`;
// Schedule the job
try {
const job = schedule.scheduleJob(cronSchedule, async function() {
// Start of maintenance window
await web.chat.postMessage({
channel: command.channel_id,
text: `🔧 *Maintenance Window Started*n*Environment:* `${environmentId}`n*Duration:* ${durationInt} hoursnnAutomatic maintenance tasks are now running.`
});
// Perform maintenance tasks
try {
// 1. Create a backup
const backupResponse = await kinstaRequest(
`/sites/environments/${environmentId}/manual-backups`,
'POST',
{ tag: `Maintenance backup ${new Date().toISOString()}` }
);
if (backupResponse && backupResponse.operation_id) {
await web.chat.postMessage({
channel: command.channel_id,
text: `✅ Maintenance backup created. Operation ID: `${backupResponse.operation_id}``
});
}
// 2. Clear cache
const cacheResponse = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (cacheResponse && cacheResponse.operation_id) {
await web.chat.postMessage({
channel: command.channel_id,
text: `✅ Cache cleared. Operation ID: `${cacheResponse.operation_id}``
});
}
// 3. Schedule end of maintenance window notification
setTimeout(async () => {
await web.chat.postMessage({
channel: command.channel_id,
text: `✅ *Maintenance Window Completed*n*Environment:* `${environmentId}`nnAll maintenance tasks have been completed.`
});
}, durationInt * 60 * 60 * 1000); // Convert hours to milliseconds
} catch (error) {
console.error('Maintenance tasks error:', error);
await web.chat.postMessage({
channel: command.channel_id,
text: `❌ Error during maintenance: ${error.message}`
});
}
});
// Store the job for later cancellation
scheduledJobs[jobId] = {
job,
taskType: 'maintenance',
environmentId,
cronSchedule,
dayOfWeek,
hour: hourInt,
duration: durationInt,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(`✅ Maintenance window scheduled!
*Environment:* `${environmentId}`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00 for ${durationInt} hours
*Job ID:* `${jobId}`
To cancel this maintenance window, use `/cancel_task ${jobId}``);
} catch (error) {
console.error('Error scheduling maintenance window:', error);
await say(`❌ Error scheduling maintenance window: ${error.message}`);
}
});
Informes automatizados
Nadie quiere despertarse cada lunes preguntándose si se ha hecho una copia de seguridad de tu sitio de WordPress o si lleva horas sin funcionar. Con los informes automatizados, tu bot de Slack puede ofrecerte a ti y a tu equipo un resumen rápido del rendimiento según un calendario.
Este tipo de informe es estupendo para controlar cosas como:
- El estado actual del sitio
- La actividad de copia de seguridad en los últimos 7 días
- Versión de PHP y dominio principal
- Cualquier bandera roja, como entornos bloqueados o copias de seguridad que faltan
Construyamos un comando /schedule_report
que automatice esto.
// Add a command to schedule weekly reporting
app.command('/schedule_report', async ({ command, ack, say }) => {
await ack();
// Expected format: environment_id day_of_week hour
// Example: /schedule_report 12345 Monday 9
const args = command.text.split(' ');
if (args.length < 3) {
await say('Please provide all required parameters. Usage: `/schedule_report [environment_id] [day_of_week] [hour]`');
return;
}
const [environmentId, dayOfWeek, hour] = args;
// Validate inputs
const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
if (!validDays.includes(dayOfWeek)) {
await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
return;
}
const hourInt = parseInt(hour, 10);
if (isNaN(hourInt) || hourInt 23) {
await say('Hour must be a number between 0 and 23.');
return;
}
// Convert day of week to cron format
const dayMap = {
'Sunday': 0,
'Monday': 1,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 4,
'Friday': 5,
'Saturday': 6
};
const cronDay = dayMap[dayOfWeek];
// Create cron schedule for the report
const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
// Generate a unique job ID
const jobId = `report_${environmentId}_${Date.now()}`;
// Schedule the job
try {
const job = schedule.scheduleJob(cronSchedule, async function() {
// Generate and send the report
await generateWeeklyReport(environmentId, command.channel_id);
});
// Store the job for later cancellation
scheduledJobs[jobId] = {
job,
taskType: 'report',
environmentId,
cronSchedule,
dayOfWeek,
hour: hourInt,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(`✅ Weekly report scheduled!
*Environment:* `${environmentId}`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00
*Job ID:* `${jobId}`
To cancel this report, use `/cancel_task ${jobId}``);
} catch (error) {
console.error('Error scheduling report:', error);
await say(`❌ Error scheduling report: ${error.message}`);
}
});
// Function to generate weekly report
async function generateWeeklyReport(environmentId, channelId) {
try {
// Get environment details
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (!response || !response.site || !response.site.environments || !response.site.environments.length) {
await web.chat.postMessage({
channel: channelId,
text: `⚠️ Weekly Report Error: No environment found with ID: `${environmentId}``
});
return;
}
const env = response.site.environments[0];
// Get backups for the past week
const backupsResponse = await kinstaRequest(`/sites/environments/${environmentId}/backups`);
let backupsCount = 0;
let latestBackup = null;
if (backupsResponse && backupsResponse.environment && backupsResponse.environment.backups) {
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const recentBackups = backupsResponse.environment.backups.filter(backup => {
const backupDate = new Date(backup.created_at);
return backupDate >= oneWeekAgo;
});
backupsCount = recentBackups.length;
if (recentBackups.length > 0) {
latestBackup = recentBackups.sort((a, b) => b.created_at - a.created_at)[0];
}
}
// Get environment status
const statusEmoji = env.is_blocked ? '🔴' : '🟢';
const statusText = env.is_blocked ? 'Blocked' : 'Running';
// Create report message
const reportDate = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
const reportMessage = `📊 *Weekly Report - ${reportDate}*
*Site:* ${env.display_name}
*Environment ID:* `${environmentId}`
*Status Summary:*
• Current Status: ${statusEmoji} ${statusText}
• PHP Version: ${env.container_info?.php_engine_version || 'Unknown'}
• Primary Domain: ${env.primaryDomain?.name || env.domains?.[0]?.name || 'N/A'}
*Backup Summary:*
• Total Backups (Last 7 Days): ${backupsCount}
• Latest Backup: ${latestBackup ? new Date(latestBackup.created_at).toLocaleString() : 'N/A'}
• Latest Backup Type: ${latestBackup ? latestBackup.type : 'N/A'}
*Recommendations:*
• ${backupsCount === 0 ? '⚠️ No recent backups found. Consider creating a manual backup.' : '✅ Regular backups are being created.'}
• ${env.is_blocked ? '⚠️ Site is currently blocked. Check for issues.' : '✅ Site is running normally.'}
_This is an automated report. For detailed information, use the `/site_status ${environmentId}` command._`;
await web.chat.postMessage({
channel: channelId,
text: reportMessage
});
} catch (error) {
console.error('Report generation error:', error);
await web.chat.postMessage({
channel: channelId,
text: `❌ Error generating weekly report: ${error.message}`
});
}
}
Gestión y supervisión de errores
Cuando tu bot empiece a realizar operaciones reales, como modificar entornos o activar tareas programadas, necesitarás algo más que console.log()
para controlar lo que ocurre en segundo plano.
Vamos a dividir esto en capas claras y fáciles de mantener:
Registro estructurado con Winston
En lugar de imprimir registros en la consola, utiliza winston
para enviar registros estructurados a archivos y, opcionalmente, a servicios como Loggly o Datadog. Instálalo con el siguiente comando:
npm install winston
A continuación, instala logger.js
:
const winston = require('winston');
const fs = require('fs');
const path = require('path');
const logsDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir);
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'wordpress-slack-bot' },
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error' }),
new winston.transports.File({ filename: path.join(logsDir, 'combined.log') })
]
});
module.exports = logger;
Luego, en tu app.js
, cambia cualquier llamada a console.log
o console.error
por:
const logger = require('./logger');
logger.info('Cache clear initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });
Enviar alertas a los administradores a través de Slack
Ya tienes la variable de entorno ADMIN_USERS
, úsala para notificar a tu equipo directamente en Slack cuando falle algo crítico:
async function alertAdmins(message, metadata = {}) {
for (const userId of ADMIN_USERS) {
const dm = await web.conversations.open({ users: userId });
const channel = dm.channel.id;
let alert = `🚨 *${message}*n`;
for (const [key, value] of Object.entries(metadata)) {
alert += `• *${key}:* ${value}n`;
}
await web.chat.postMessage({ channel, text: alert });
}
}
Úsala así:
await alertAdmins('Backup Failed', {
environmentId,
error: error.message,
user: ``
});
Sigue el rendimiento con métricas básicas
No te lances de lleno a Prometheus si sólo quieres ver el estado de tu bot. Mantén un objeto de rendimiento ligero:
const metrics = {
apiCalls: 0,
errors: 0,
commands: 0,
totalTime: 0,
get avgResponseTime() {
return this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
}
};
Actualízalo dentro de tu ayudante kinstaRequest()
:
const start = Date.now();
try {
metrics.apiCalls++;
const res = await fetch(...);
return await res.json();
} catch (err) {
metrics.errors++;
throw err;
} finally {
metrics.totalTime += Date.now() - start;
}
Exponlo mediante un comando como /bot_performance
:
app.command('/bot_performance', async ({ command, ack, say }) => {
await ack();
if (!ADMIN_USERS.includes(command.user_id)) {
return await say('⛔ Not authorized.');
}
const msg = `📊 *Bot Metrics*
• API Calls: ${metrics.apiCalls}
• Errors: ${metrics.errors}
• Avg Response Time: ${metrics.avgResponseTime.toFixed(2)}ms
• Commands Run: ${metrics.commands}`;
await say(msg);
});
Opcional: Definir pasos de recuperación
Si quieres implementar una lógica de recuperación (como reintentar borrar el caché a través de SSH), sólo tienes que crear un ayudante como:
async function attemptRecovery(environmentId, issue) {
logger.warn('Attempting recovery', { environmentId, issue });
if (issue === 'cache_clear_failure') {
// fallback logic here
}
// Return a recovery status object
return { success: true, message: 'Fallback ran.' };
}
Mantenlo fuera de tu lógica de comando principal, a menos que sea una ruta crítica. En muchos casos, es mejor registrar el error, alertar a los administradores y dejar que los humanos decidan qué hacer.
Despliega y gestiona tu Slackbot
Una vez que tu bot esté completo, debes desplegarlo en un entorno de producción donde pueda funcionar 24 horas al día, 7 días a la semana.
Sevalla de Kinsta es un lugar excelente para alojar bots como éste. Es compatible con aplicaciones Node.js, variables de entorno, registro y despliegues escalables.
Alternativamente, puedes contenerizar tu bot utilizando Docker o desplegarlo en cualquier plataforma en la nube que admita Node.js y servicios en segundo plano.
Aquí tienes algunas cosas que debes tener en cuenta antes de ponerte en marcha:
- Utiliza variables de entorno para todos los secretos (tokens de Slack, claves API de Kinsta, claves SSH).
- Configura el registro y la monitorización del tiempo de actividad para saber cuándo se rompe algo.
- Ejecuta tu bot con un gestor de procesos como PM2 o la política
restart: always
de Docker para mantenerlo vivo después de caídas o reinicios. - Mantén seguras tus claves SSH, especialmente si las utilizas para la automatización.
Resumen
Ahora has convertido tu Slackbot de un simple gestor de comandos a una potente herramienta con interactividad real, automatización programada y una monitorización sólida. Estas funcionalidades hacen que tu bot sea más útil, más fiable y mucho más agradable de usar, especialmente para equipos que gestionan varios sitios de WordPress.
Y cuando combinas esto con la potencia de la API de Kinsta y el alojamiento sin estrés de Kinsta, tienes una configuración escalable y fiable.