Gli Slackbot non devono rimanere in attesa che vengano digitati i comandi. Con la configurazione giusta, un bot può permettere di gestire i siti WordPress con pulsanti interattivi, menu a tendina, attività programmate e avvisi intelligenti, il tutto all’interno di Slack.

In questo articolo mostreremo come aggiungere funzioni di interattività, automazione e monitoraggio ad un bot Slack.

Prerequisiti

Prima di iniziare, è necessario assicurasi di avere:

  • Un’app Slack con permessi bot e un comando slash.
  • Un account Kinsta con accesso alle API e un sito da testare.
  • Node.js e NPM installati in locale.
  • Una familiarità di base con JavaScript (o almeno una certa dimestichezza nel copiare e modificare il codice).
  • Chiavi API per Slack e Kinsta.

Come iniziare

Per costruire questo Slackbot, utilizziamo Node.js e il framework Bolt di Slack per collegare i comandi slash che attivano le azioni tramite l’API di Kinsta.

In questa guida non ripeteremo tutti i passaggi per la creazione di un’app Slack o per ottenere l’accesso all’API di Kinsta, in quanto questi aspetti sono già stati trattati nella nostra guida precedente, Come costruire uno Slackbot con Node.js e l’API di Kinsta per la gestione di un sito.

Consigliamo a chi non l’avesse ancora letta, di leggerla prima. La guida spiega come creare un’app Slack, come ottenere il token del bot e il segreto di firma e come ottenere la chiave API di Kinsta.

Aggiungere interattività ad uno Slackbot

Gli Slackbot non devono affidarsi solo ai comandi slash. Con componenti interattivi come pulsanti, menu e finestre modali, possiamo trasformare un bot in uno strumento molto più intuitivo.

Invece di digitare /clear_cache environment_id, immaginiamo di cliccare su un pulsante con la dicitura Clear Cache subito dopo aver controllato lo stato di un sito. Per farlo, abbiamo bisogno del client Web API di Slack. Possiamo installarlo nel progetto con il seguente comando:

npm install @slack/web-api

Poi inizializziamolo nel nostro sito app.js:

const { WebClient } = require('@slack/web-api');
const web = new WebClient(process.env.SLACK_BOT_TOKEN);

Assicuriamoci che nel file .env sia impostato SLACK_BOT_TOKEN. Ora miglioriamo il comando /site_status dell’articolo precedente. Invece di inviare solo del testo, aggiungiamo dei pulsanti per azioni rapide come Cancella cache, Crea backup o Controlla stato dettagliato.

Ecco come appare il gestore aggiornato:

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}`);
  }
});

Ogni clic sul pulsante attiva un’azione. Ecco come gestiamo il pulsante Cancella cache:

// 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}`);
  }
});

Possiamo seguire lo stesso schema per i pulsanti di backup e di stato, collegando ciascuno di essi all’endpoint dell’API o alla logica di comando appropriata.

// 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
  // ...
});

Un menu a tendina per selezionare un sito

Digitare gli ID degli ambienti non è divertente. E aspettarsi che ogni membro del team ricordi quale ID appartiene a quale ambiente? Non è realistico.

Rendiamo il tutto più intuitivo. Invece di chiedere agli utenti di digitare /site_status [environment-id], daremo loro un menu a tendina di Slack in cui potranno scegliere un sito da un elenco. Una volta selezionato, il bot ne mostrerà lo stato e allegherà gli stessi pulsanti di azione rapida che abbiamo implementato in precedenza.

Per farlo:

  • recuperiamo tutti i siti dall’API di Kinsta
  • recuperiamo gli ambienti per ogni sito
  • creiamo un menu a tendina con queste opzioni
  • gestiamo la selezione dell’utente e visualizziamo lo stato del sito

Ecco il comando che mostra il menu a tendina:

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}`);
  }
});

Quando l’utente sceglie un sito, lo gestiamo con questo gestore di azioni:

// 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}`);
  }
});

Ora che il nostro bot può attivare azioni con un pulsante e selezionare siti da un elenco, assicuriamoci di fare in modo che non vengano eseguite accidentalmente operazioni rischiose.

Finestre di conferma

Alcune operazioni non dovrebbero mai essere eseguite accidentalmente. Cancellare la cache può sembrare un’operazione innocua, ma se si lavora su un sito di produzione, probabilmente non bisognerà farlo con un solo clic, soprattutto se si sta solo controllando lo stato del sito. È qui che entrano in gioco i modali (finestre di dialogo) di Slack.

Invece di cancellare immediatamente la cache cliccando su clear_cache_button, è preferibile mostrare una finestra di conferma. Ecco come fare:

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);
  }
});

Nel codice qui sopra, utilizziamo web.views.open() per lanciare una finestra modale con un titolo chiaro, un messaggio di avviso e due pulsanti – Clear Cache e Cancel – e memorizziamo environmentId in private_metadata in modo da averlo a disposizione quando l’utente clicca su Clear Cache.

Una volta che l’utente abbia cliccato sul pulsante Clear Cache del modale, Slack invia un evento view_submission. Ecco come gestirlo e come procedere con l’operazione vera e propria:

// 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}`
    });
  }
});

In questo codice, dopo la conferma dell’utente, prendiamo l’evento environmentId da private_metadata, apriamo un DM privato usando web.conversations.open() per evitare di ingombrare i canali pubblici, eseguiamo la richiesta API per cancellare la cache e proseguiamo con un messaggio di successo o di errore a seconda del risultato.

Indicatori di avanzamento

Alcuni comandi di Slack sono immediati, come la cancellazione della cache o la verifica dello stato. Ma non tutti.

Creare un backup o distribuire dei file può richiedere diversi secondi o addirittura minuti. Se durante questo tempo il bot rimane in silenzio, gli utenti potrebbero pensare che si sia rotto qualcosa.

Slack non offre una barra di avanzamento nativa, ma possiamo simulare una barra di avanzamento con un po’ di creatività. Ecco una funzione di aiuto che aggiorna un messaggio con una barra di avanzamento visiva utilizzando il kit dei blocchi:

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}``
        }
      }
    ]
  });
}

Integriamo questa funzione in un comando di /create_backup. Prima di rispondere, invece di aspettare il completamento dell’intera operazione, controlleremo l’utente a ogni passaggio.

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}`
    });
  }
});

Notifiche di conferma/errore

Al momento, il nostro bot probabilmente invia un testo semplice come ✅ Success o ❌ Failed. Funziona, ma è scarno e non aiuta gli utenti a capire perché qualcosa è andato a buon fine o cosa devono fare in caso di errore.

Risolviamo il problema con una formattazione corretta per i messaggi di conferma e di errore, con un contesto utile, suggerimenti e una formattazione pulita.

Aggiungiamo queste utility al nostro sito utils.js in modo da poterle riutilizzare per tutti i comandi:

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
};

Queste funzioni prendono input strutturati e li trasformano in markdown adatti a Slack con emoji, etichette e interruzioni di riga. Molto più facile da comprendere nel bel mezzo di un’intensa discussione su Slack. Ecco come appare all’interno di un vero gestore di comandi. Utilizziamo come esempio /clear_cache:

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'
      ]
    ));
  }
});

Automatizzare le attività di WordPress con operazioni programmate

Finora, tutto ciò che il nostro Slackbot fa avviene quando qualcuno attiva esplicitamente un comando. Ma non tutto deve dipendere dal fatto che qualcuno si ricordi di eseguire un comando.

E se il nostro bot potesse eseguire automaticamente il backup dei siti ogni sera? E se potesse controllare se un sito è giù ogni mattina prima che il team si svegli.

Utilizzeremo la libreria node-schedule per eseguire attività basate su espressioni cron. Per prima cosa, installiamola con:

npm install node-schedule

Ora impostiamola all’inizio dell’app.js del nostro sito:

const schedule = require('node-schedule');

Avremo anche bisogno di un modo per tenere traccia delle operazioni programmate attive, in modo che gli utenti possano listarle o cancellarle:

const scheduledJobs = {};

Creare il comando di programmazione delle attività

Inizieremo con un comando di base /schedule_task che accetta un tipo di attività (backup, clear_cache o status_check), l’ID dell’ambiente e un’espressione cron.

/schedule_task backup 12345 0 0 * * *

Questo comando programma un backup giornaliero a mezzanotte. Ecco il gestore completo del comando:

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}`);
  }
});

Annullare le attività pianificate

Se qualcosa cambia o se l’attività non è più necessaria, gli utenti possono annullarla con:

/cancel_task

Ecco l’implementazione:

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.`);
});

Elenco di tutte le attività programmate

Permettiamo agli utenti di visualizzare tutti i lavori che sono stati programmati:

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);
});

Questo dà al nostro Slackbot un nuovo livello di autonomia. I backup, la pulizia della cache e i controlli di stato non devono più essere compito di qualcuno. Avranno luogo in modo silenzioso, affidabile e puntuale.

Manutenzione periodica

A volte si desidera eseguire un gruppo di attività di manutenzione a intervalli regolari, come i backup settimanali e la pulizia della cache la domenica sera. È qui che entrano in gioco le finestre di manutenzione.

Una finestra di manutenzione è un intervallo di tempo programmato in cui il bot esegue automaticamente attività predefinite come:

  • Creare un backup
  • Cancellare la cache
  • Inviare notifiche di avvio e completamento

Il formato è semplice:

/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]

Ad esempio:

/maintenance_window 12345 Sunday 2 3

Questo significa che ogni domenica alle 2 del mattino, le attività di manutenzione vengono eseguite per 3 ore. Ecco l’implementazione 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}`);
  }
});

Reportistica automatizzata

Non è bello svegliarsi ogni lunedì chiedendosi se il sito è stato sottoposto a backup o se è stato giù per ore. Con i report automatici, il nostro bot Slack può fornire a noi e al nostro team un riepilogo delle prestazioni in base a un calendario.

Questo tipo di report è ottimo per tenere sotto controllo aspetti quali:

  • Stato attuale del sito
  • Operazioni di backup degli ultimi 7 giorni
  • Versione di PHP e dominio primario
  • Eventuali bandiere rosse, come ambienti bloccati o backup mancanti

Creiamo un comando /schedule_report che automatizzi tutte queste operazioni.

// 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}`
    });
  }
}

Gestione degli errori e monitoraggio

Quando il nostro bot inizia a eseguire operazioni reali come la modifica degli ambienti o l’attivazione di attività programmate, c’è bisogno di qualcosa di più di console.log() per tenere traccia di ciò che accade dietro le quinte.

Vediamo di suddividere il tutto in livelli puliti e manutenibili:

Registrazione strutturata con Winston

Invece di stampare i registri nella console, usiamo il comando winston per inviare log strutturati a file e, facoltativamente, a servizi come Loggly o Datadog. Installiamolo con il seguente comando:

npm install winston

Quindi configuriamo 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;

Quindi, nel nostro file app.js, sostituiamo le chiamate a console.log o console.error con:

const logger = require('./logger');

logger.info('Cache clear initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });

Inviare avvisi agli amministratori via Slack

Abbiamo già la variabile env ADMIN_USERS. La useremo per avvisare il nostro team direttamente su Slack quando qualcosa di critico non funziona:

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 });
  }
}

La utilizziamo in questo modo:

await alertAdmins('Backup Failed', {
  environmentId,
  error: error.message,
  user: ``
});

Tracciare le prestazioni con indicatori di base

Se vogliamo solo sapere quanto è in salute il nostro bot, non dobbiamo fare il passo più lungo della gamba con Prometheus. Manteniamo un leggero oggetto delle performance:

const metrics = {
  apiCalls: 0,
  errors: 0,
  commands: 0,
  totalTime: 0,
  get avgResponseTime() {
    return this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
  }
};

Aggiorniamolo all’interno del nostro helper 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;
}

Esponiamolo tramite un comando come /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);
});

Opzionale: Definire le fasi di recupero

Se decidiamo di implementare una logica di recupero (come riprovare la cancellazione della cache via SSH), possiamo creare un helper come:

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.' };
}

Teniamolo fuori dalla logica del comando principale, a meno che non si tratti di un percorso critico. In molti casi, è meglio registrare l’errore, avvisare gli amministratori e lasciare che siano gli umani a decidere cosa fare.

Distribuire e gestire lo Slackbot

Una volta che il nostro bot è completo di tutte le funzionalità, dobbiamo distribuirlo in un ambiente di produzione dove possa funzionare 24/7.

Sevalla di Kinsta è un luogo eccellente per l’hosting di questo tipo di bot. Supporta le applicazioni Node.js, le variabili d’ambiente, il logging e le distribuzioni scalabili.

In alternativa, si può containerizzare il bot utilizzando Docker o distribuirlo su qualsiasi piattaforma cloud che supporti Node.js e i servizi in background.

Ecco alcune cose da tenere a mente prima di iniziare:

  • Usiamo le variabili d’ambiente per tutti i segreti (token Slack, chiavi API Kinsta, chiavi SSH).
  • Impostiamo la registrazione e il monitoraggio dell’attività in modo da sapere quando qualcosa si interrompe.
  • Eseguiamo il nostro bot con un gestore di processi come PM2 o la policy restart: always di Docker per mantenerlo in vita anche dopo i crash o i riavvii.
  • Teniamo al sicuro le chiavi SSH, soprattutto se le utilizziamo per l’automazione.

Riepilogo

Abbiamo trasformato il nostro Slackbot da un semplice gestore di comandi a un potente strumento con interattività reale, automazione programmata e un solido monitoraggio. Queste caratteristiche rendono il nostro bot più utile, più affidabile e molto più piacevole da usare, soprattutto per i team che gestiscono più siti WordPress.

E se a questo si aggiunge la potenza dell’API e dell’hosting di Kinsta, si ha una configurazione scalabile e affidabile.

Joel Olawanle Kinsta

Joel è uno Frontend developer che lavora in Kinsta come redattore tecnico. È un insegnante appassionato che ama l'open source e ha scritto oltre 200 articoli tecnici principalmente su JavaScript e i suoi framework.