Les Slackbots n’ont pas besoin d’attendre que vous saisissiez des commandes. Avec la bonne configuration, votre robot peut vous aider à gérer vos sites WordPress en proposant des boutons interactifs, des menus déroulants, des tâches planifiées et des alertes intelligentes, le tout au sein même de Slack.

Dans cet article, nous allons vous montrer comment ajouter de l’interactivité, de l’automatisation et de la surveillance à votre robot Slack.

Conditions préalables

Avant de commencer, assurez-vous d’avoir :

  • Une application Slack avec des permissions de robot et une commande slash.
  • Un compte Kinsta avec un accès à l’API et un site à tester.
  • Node.js et NPM installés localement.
  • Une familiarité de base avec JavaScript (ou au moins être à l’aise pour copier et modifier du code).
  • Des clés API pour Slack et Kinsta.

Pour commencer

Pour construire ce Slackbot, Node.js et le framework Bolt de Slack sont utilisés pour câbler des commandes slash qui déclenchent des actions via l’API de Kinsta.

Nous ne reviendrons pas sur toutes les étapes de la création d’une application Slack ou de l’obtention d’un accès à l’API Kinsta dans ce guide, car elles ont déjà été abordées dans notre guide précédent, Comment créer un Slackbot avec Node.js et l’API Kinsta pour la gestion de site.

Si vous n’avez pas encore vu ce dernier, lisez-le d’abord. Il vous explique comment créer votre application Slack, obtenir votre jeton de robot et votre secret de signature, et obtenir votre clé API Kinsta.

Ajouter de l’interactivité à votre Slackbot

Les Slackbots n’ont pas besoin de s’appuyer uniquement sur les commandes slash. Avec des composants interactifs tels que des boutons, des menus et des fenêtres modales, vous pouvez transformer votre robot en un outil beaucoup plus intuitif et convivial.

Au lieu de saisir /clear_cache environment_id, imaginez que vous cliquiez sur un bouton intitulé Vider le cache juste après avoir vérifié l’état d’un site. Pour cela, vous avez besoin du client API Web de Slack. Installez-le dans votre projet à l’aide de la commande ci-dessous :

npm install @slack/web-api

Puis initialisez-le dans votre app.js:

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

Assurez-vous que SLACK_BOT_TOKEN est défini dans votre fichier .env. Maintenant, améliorons la commande /site_status de l’article précédent. Au lieu d’envoyer simplement du texte, nous attachons des boutons pour des actions rapides comme Vider le cache, Créer une sauvegarde ou Vérifier l’état détaillé.

Voici à quoi ressemble le gestionnaire mis à jour :

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

Chaque clic sur un bouton déclenche une action. Voici comment nous gérons le bouton Vider le 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}`);
  }
});

Vous pouvez suivre le même schéma pour les boutons de sauvegarde et d’état, en reliant simplement chacun d’eux au point de terminaison API ou à la logique de commande appropriés.

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

Utiliser une liste déroulante pour sélectionner un site

Saisir les identifiants d’environnement n’est pas très amusant. Et attendre de chaque membre de l’équipe qu’il se souvienne de quel identifiant appartient à quel environnement ? Ce n’est pas réaliste.

Rendons cela plus intuitif. Au lieu de demander aux utilisateurs de saisir /site_status [environment-id], nous leur donnerons un menu déroulant Slack où ils pourront choisir un site dans une liste. Une fois qu’ils en auront sélectionné un, le robot affichera le statut et attachera les mêmes boutons d’action rapide que nous avons mis en œuvre plus tôt.

Pour cela, nous :

  • Récupérons tous les sites à partir de l’API Kinsta.
  • Récupérons les environnements pour chaque site
  • Construisons un menu déroulant avec ces options
  • Gérons la sélection de l’utilisateur et afficher le statut du site.

Voici la commande qui affiche le menu déroulant :

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

Lorsqu’un utilisateur choisit un site, nous le gérons avec ce gestionnaire d’action :

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

Maintenant que notre robot peut déclencher des actions avec un bouton et sélectionner des sites dans une liste, assurons-nous de ne pas exécuter accidentellement des opérations risquées.

Dialogues de confirmation

Certaines opérations ne devraient jamais être exécutées accidentellement. Vider un cache peut sembler inoffensif, mais si vous travaillez sur un site de production, vous n’avez probablement pas envie de le faire d’un simple clic – surtout si vous ne faisiez que vérifier l’état du site. C’est là que les modales (dialogues) de Slack entrent en jeu.

Au lieu d’effacer immédiatement le cache lorsque l’on clique sur clear_cache_button, nous affichons une fenêtre modale de confirmation. Voici comment procéder :

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

Dans le code ci-dessus, nous utilisons web.views.open() pour lancer une fenêtre modale avec un titre clair, un message d’avertissement et deux boutons – Vider le cache et Annuler – et nous stockons environmentId dans private_metadata pour l’avoir lorsque l’utilisateur clique sur Vider le cache.

Lorsque l’utilisateur clique sur le bouton Vider le cache dans la fenêtre modale, Slack envoie un événement view_submission. Voici comment le gérer et procéder à l’opération proprement dite :

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

Dans ce code, une fois que l’utilisateur a confirmé, nous saisissons le environmentId à partir de private_metadata, ouvrons un DM privé à l’aide de web.conversations.open() pour éviter d’encombrer les canaux publics, exécutons la requête API pour vider le cache, et suivons avec un message de réussite ou d’erreur en fonction du résultat.

Dialogues de confirmation

Certaines commandes Slack sont instantanées, comme vider un cache ou vérifier un statut. Mais d’autres ? Pas tant que ça.

La création d’une sauvegarde ou le déploiement de fichiers peut prendre plusieurs secondes, voire plusieurs minutes. Et si votre robot reste silencieux pendant tout ce temps, les utilisateurs pourraient supposer que quelque chose s’est cassé.

Slack ne vous donne pas de barre de progression native, mais nous pouvons en simuler une avec un peu de créativité. Voici une fonction d’aide qui met à jour un message avec une barre de progression visuelle à l’aide d’un kit de blocs :

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

Intégrons ceci dans une commande /create_backup. Au lieu d’attendre que toute l’opération soit terminée avant de répondre, nous prendrons des nouvelles de l’utilisateur à chaque étape.

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

Notifications de succès/échec

À l’heure actuelle, votre robot renvoie probablement du texte brut comme ✅ Succès ou ❌ Échec. Cela fonctionne, mais c’est fade, et cela n’aide pas les utilisateurs à comprendre pourquoi quelque chose a réussi ou ce qu’ils doivent faire en cas d’échec.

Réparons cela avec une mise en forme appropriée pour les messages de réussite et d’erreur aux côtés d’un contexte utile, de suggestions et d’une mise en forme propre.

Ajoutez ces utilitaires à ton site utils.js pour pouvoir les réutiliser dans toutes les commandes :

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

Ces fonctions prennent les entrées structurées et les transforment en markdown adapté à Slack avec des emoji, des étiquettes et des sauts de ligne. C’est beaucoup plus facile à scanner au milieu d’un fil de discussion Slack très animé. Voici à quoi cela ressemble à l’intérieur d’un véritable gestionnaire de commandes. Prenons l’exemple de /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'
      ]
    ));
  }
});

Automatiser les tâches WordPress avec des tâches planifiées

Jusqu’à présent, tout ce que fait votre Slackbot se produit lorsque quelqu’un déclenche explicitement une commande. Mais tout ne doit pas dépendre du fait que quelqu’un se souvienne de l’exécuter.

Et si votre Slackbot pouvait automatiquement sauvegarder vos sites tous les soirs ? Ou vérifier si un site est en panne tous les matins avant que l’équipe ne se réveille.

Nous allons utiliser la bibliothèque node-schedule pour exécuter des tâches basées sur des expressions cron. Tout d’abord, installez-la :

npm install node-schedule

Maintenant, installez-la en haut de votre app.js:

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

Nous aurons également besoin d’un moyen de suivre les travaux planifiés actifs afin que les utilisateurs puissent les lister ou les annuler plus tard :

const scheduledJobs = {};

Création de la commande schedule task

Nous commencerons par une commande de base /schedule_task qui accepte un type de tâche (backup, clear_cache, ou status_check), l’identifiant de l’environnement et une expression cron.

/schedule_task backup 12345 0 0 * * *

Cette commande planifierait une sauvegarde quotidienne à minuit. Voici le gestionnaire de commande complet :

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

Annulation des tâches planifiées

Si quelque chose change ou si la tâche n’est plus nécessaire, les utilisateurs peuvent l’annuler avec :

/cancel_task

Voici la mise en œuvre :

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

Lister toutes les tâches planifiées

Permettons également aux utilisateurs d’afficher toutes les tâches qui ont été planifiées :

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

Cela donne à votre Slackbot un tout nouveau niveau d’autonomie. Les sauvegardes, les vidages de cache et les vérifications d’état n’ont plus besoin d’être le travail de quelqu’un. Elles se font tranquillement, de manière fiable et à l’heure prévue.

Maintenance récurrente

Parfois, vous voulez exécuter un groupe de tâches de maintenance à intervalles réguliers, comme les sauvegardes hebdomadaires et les vidages de cache le dimanche soir. C’est là qu’interviennent les fenêtres de maintenance.

Une fenêtre de maintenance est un bloc de temps programmé pendant lequel le robot exécute automatiquement des tâches prédéfinies telles que :

  • La création d’une sauvegarde
  • Vider le cache
  • L’envoi de notifications de démarrage et d’achèvement

Le format est simple :

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

Par exemple :

/maintenance_window 12345 Sunday 2 3

Cela signifie que tous les dimanches à 2 heures du matin, les tâches de maintenance sont exécutées pendant 3 heures. Voici la mise en œuvre complète :

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

Rapports automatisés

Vous ne voulez pas vous réveiller tous les lundis en vous demandant si votre site WordPress a été sauvegardé ou s’il est en panne depuis des heures. Grâce aux rapports automatisés, votre robot Slack peut vous donner, ainsi qu’à votre équipe, un résumé rapide des performances selon un calendrier établi.

Ce type de rapport est idéal pour garder un œil sur des choses comme :

  • L’état actuel du site
  • L’activité de sauvegarde au cours des 7 derniers jours
  • La version de PHP et le domaine principal
  • Les signaux d’alarme, comme les environnements bloqués ou les sauvegardes manquantes.

Construisons une commande /schedule_report qui automatise tout cela.

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

Gestion des erreurs et surveillance

Une fois que votre robot commence à effectuer de vraies opérations comme la modification d’environnements ou le déclenchement de tâches programmées, vous avez besoin de plus que console.log() pour suivre ce qui se passe en coulisses.

Décomposons cela en couches propres et faciles à maintenir :

Journalisation structurée avec Winston

Au lieu d’imprimer les journaux sur la console, utilise winston pour envoyer des journaux structurés vers des fichiers, et éventuellement vers des services comme Loggly ou Datadog. Installez-le avec la commande ci-dessous :

npm install winston

Ensuite, installez 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;

Ensuite, dans votre app.js, remplacez tous les appels console.log ou console.error par :

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

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

Envoyer des alertes aux administrateurs via Slack

Vous avez déjà la variable env ADMIN_USERS, utilisez-la pour avertir votre équipe directement dans Slack lorsque quelque chose de critique échoue :

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

Utilisez-la comme ceci :

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

Suivre les performances avec des mesures de base

Ne vous lancez pas dans un Prometheus complet si vous essayez juste de voir si votre robot est en bonne santé. Gardez un objet de performance léger :

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

Mettez-le à jour dans votre 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;
}

Exposez-le via une commande comme /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);
});

Facultatif : Définir les étapes de récupération

Si vous voulez mettre en place une logique de récupération (comme réessayer de vider le cache via SSH), il vous suffit de créer un helper comme :

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

Tenez-le à l’écart de votre logique de commande principale à moins qu’il ne s’agisse d’un chemin critique. Dans de nombreux cas, il est préférable d’enregistrer l’erreur, d’alerter les administrateurs et de laisser les humains décider de ce qu’il faut faire.

Déployer et gérer votre Slackbot

Une fois que votre Slackbot est doté de toutes les fonctionnalités, vous devez le déployer dans un environnement de production où il peut fonctionner 24/7.

Sevalla de Kinsta est un excellent endroit pour héberger des robots comme celui-ci. Il prend en charge les applications Node.js, les variables d’environnement, la journalisation et les déploiements évolutifs dès le départ.

Sinon, vous pouvez conteneuriser votre robot à l’aide de Docker ou le déployer sur n’importe quelle plateforme cloud qui prend en charge Node.js et les services d’arrière-plan.

Voici quelques points à garder à l’esprit avant de passer à l’action :

  • Utilisez des variables d’environnement pour tous les secrets (jetons Slack, clés API Kinsta, clés SSH).
  • Configurez la journalisation et la surveillance du temps de fonctionnement pour que vous sachiez quand quelque chose se casse.
  • Exécutez votre robot avec un gestionnaire de processus comme PM2 ou la politique restart: always de Docker pour le maintenir en vie après les pannes ou les redémarrages.
  • Gardez vos clés SSH en sécurité, surtout si vous les utilisez pour l’automatisation.

Résumé

Vous avez maintenant fait passer votre Slackbot d’un simple gestionnaire de commandes à un outil puissant doté d’une réelle interactivité, d’une automatisation programmée et d’une surveillance solide. Ces caractéristiques rendent votre robot plus utile, plus fiable et bien plus agréable à utiliser, en particulier pour les équipes qui gèrent plusieurs sites WordPress.

Et quand vous associez cela à la puissance de l’API Kinsta et à l’hébergement sans stress de Kinsta, vous obtenez une configuration à la fois évolutive et fiable.

Joel Olawanle Kinsta

Joel est un développeur d'interfaces publiques qui travaille chez Kinsta en tant que rédacteur technique. Il est un enseignant passionné par l'open source et a écrit plus de 200 articles techniques, principalement autour de JavaScript et de ses frameworks.