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.