Slackbots hoeven niet te wachten tot jij commando’s intypt. Met de juiste instellingen kan je bot helpen bij het beheren van je WordPress sites door interactieve knoppen, dropdowns, geplande taken en slimme waarschuwingen aan te bieden – allemaal direct in Slack.
In dit artikel laten we je zien hoe je interactiviteit, automatisering en monitoring kunt toevoegen aan je Slack bot.
Vereisten
Voordat je begint, moet je ervoor zorgen dat je beschikt over:
- Een Slack App met botrechten en een slash commando.
- Een Kinsta account met API toegang en een site om mee te testen.
- Node.js en NPM lokaal geïnstalleerd hebt.
- Basiskennis van JavaScript (of op zijn minst comfortabel met het kopiëren en tweaken van code).
- API sleutels voor Slack en Kinsta.
Zo begin je
Om deze Slackbot te bouwen, worden Node.js en Slack’s Bolt framework gebruikt om slash commando’s te verbinden die acties triggeren via de Kinsta API.
We zullen niet elke stap van het maken van een Slack app of het verkrijgen van Kinsta API toegang herhalen in deze gids, omdat die al behandeld zijn in onze eerdere gids, Zo bouw je een Slackbot met Node.js en Kinsta API voor sitebeheer.
Als je die nog niet hebt gezien, lees hem dan eerst. Het leidt je door het maken van je Slack app, het verkrijgen van je bot token en signing secret, en het verkrijgen van je Kinsta API sleutel.
Interactiviteit toevoegen aan je Slackbot
Slackbots hoeven niet alleen op slash commando’s te vertrouwen. Met interactieve componenten zoals knoppen, menu’s en modals kun je van je bot een veel intuïtievere en gebruiksvriendelijkere tool maken.
Stel je voor dat je in plaats van /clear_cache environment_id
op een knop met het label Clear Cache klikt nadat je de status van een site hebt gecontroleerd. Om dit te doen, heb je de Web API client van Slack nodig. Installeer deze in je project met het onderstaande commando:
npm install @slack/web-api
Initialiseer het dan in je app.js
:
const { WebClient } = require('@slack/web-api');
const web = new WebClient(process.env.SLACK_BOT_TOKEN);
Zorg ervoor dat SLACK_BOT_TOKEN
is ingesteld in je .env
bestand. Laten we nu het /site_status
commando uit het vorige artikel verbeteren. In plaats van alleen tekst te sturen, voegen we knoppen toe voor snelle acties zoals Clear Cache, Create Backup of Detailed Status.
Zo ziet de bijgewerkte handler eruit:
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}`);
}
});
Elke klik op een knop activeert een actie. Dit is hoe we de knop Clear Cache afhandelen:
// 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}`);
}
});
Je kunt hetzelfde patroon volgen voor de backup- en statusknoppen, door ze te koppelen aan het juiste API eindpunt of opdrachtlogica.
// 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
// ...
});
Een dropdown gebruiken om een site te selecteren
Het intypen van omgevings-ID’s is niet leuk. En verwachten dat elk teamlid onthoudt welke ID bij welke omgeving hoort? Dat is niet realistisch.
Laten we het intuïtiever maken. In plaats van gebruikers te vragen om /site_status [environment-id]
in te typen, geven we ze een Slack dropdown waar ze een site uit een lijst kunnen kiezen. Zodra ze er een hebben gekozen, zal de bot de status laten zien en dezelfde sneltoetsen toevoegen die we eerder hebben geïmplementeerd.
Om dit te doen moeten we:
- Alle sites ophalen uit de Kinsta API
- De omgevingen voor elke site ophalen
- Een vervolgkeuzemenu bouwen met deze opties
- De selectie van de gebruiker afhandelen en de status van de site weergeven
Hier is het commando dat de dropdown toont:
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}`);
}
});
Wanneer een gebruiker een site kiest, handelen we dat af met deze action handler:
// 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}`);
}
});
Nu onze bot acties kan triggeren met een knop en sites kan selecteren uit een lijst, moeten we ervoor zorgen dat we niet per ongeluk riskante handelingen uitvoeren.
Bevestigingsvensters
Sommige handelingen mogen nooit per ongeluk worden uitgevoerd. Een cache wissen klinkt misschien onschuldig, maar als je aan een productiesite werkt, wil je dat waarschijnlijk niet met één klik doen – zeker niet als je alleen maar de sitestatus aan het controleren was. Dat is waar Slack modals (dialoogvensters) van pas kunnen komen.
In plaats van meteen de cache te wissen als er op clear_cache_button
wordt geklikt, laten we een bevestigingsmodal zien. Zo werkt het:
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);
}
});
In de bovenstaande code gebruiken we web.views.open()
om een modal te starten met een duidelijke titel, een waarschuwingsbericht en twee knoppen – Clear Cache en Cancel – en slaan we de environmentId
op in private_metadata
zodat we die hebben wanneer de gebruiker op Clear Cache klikt.
Zodra de gebruiker op de knop Clear Cache in het modal klikt, stuurt Slack een view_submission
event. Dit is hoe we het afhandelen en verder gaan met de eigenlijke bewerking:
// 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 deze code pakken we, nadat de gebruiker heeft bevestigd, de environmentId
van private_metadata
, openen we een privé DM met web.conversations.open()
om te voorkomen dat openbare kanalen vollopen, voeren we het API-verzoek uit om de cache te wissen en geven we een succes- of foutmelding, afhankelijk van het resultaat.
Voortgangsindicatoren
Sommige Slack commando’s zijn direct, zoals het wissen van een cache of het controleren van een status. Maar andere? Niet zo direct.
Het maken van een backup of het implementeren van bestanden kan enkele seconden of zelfs minuten duren. En als je bot in die tijd niet zoveel doet, kunnen gebruikers aannemen dat er iets niet werkt.
Slack geeft je geen eigen voortgangsbalk, maar we kunnen er een namaken met een beetje creativiteit. Hier is een hulpfunctie die een bericht bijwerkt met een visuele voortgangsbalk met behulp van block kit:
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}``
}
}
]
});
}
Laten we dit integreren in een /create_backup
commando. In plaats van te wachten tot de hele operatie is voltooid voordat we antwoorden, controleren we de gebruiker bij elke stap.
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}`
});
}
});
Meldingen bij succes/mislukking
Op dit moment stuurt je bot waarschijnlijk platte tekst terug zoals ✅ Succes of ❌ Failed. Het werkt, maar het is nietszeggend en het helpt gebruikers niet te begrijpen waarom iets gelukt is of wat ze moeten doen als het mislukt.
Laten we dat oplossen met de juiste opmaak voor succes- en foutmeldingen naast nuttige context, suggesties en een schone opmaak.
Voeg deze tools toe aan je utils.js
zodat je ze voor alle commando’s kunt hergebruiken:
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
};
Deze functies nemen gestructureerde invoer en zetten het om in Slack vriendelijke markdown met emoji, labels en regeleinden. Veel gemakkelijker om te scannen midden in een drukke Slack thread. Hier zie je hoe dat eruit ziet in een echte commando-afhandeling. Laten we /clear_cache
als voorbeeld gebruiken:
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'
]
));
}
});
WordPress taken automatiseren met geplande opdrachten
Tot nu toe gebeurt alles wat je Slackbot doet wanneer iemand expliciet een commando start. Maar niet alles zou afhankelijk moeten zijn van iemand die eraan denkt om het uit te voeren.
Wat als je bot elke nacht automatisch een backup kan maken van je sites? Of elke ochtend controleren of een site down is voordat het team wakker wordt.
We gebruiken de node-schedule bibliotheek om taken uit te voeren op basis van cron expressies. Installeer het eerst:
npm install node-schedule
Stel het nu in bovenaan je app.js
:
const schedule = require('node-schedule');
We hebben ook een manier nodig om actieve geplande taken bij te houden, zodat gebruikers ze later kunnen tonen of annuleren:
const scheduledJobs = {};
Het commando voor het plannen van taken maken
We beginnen met een basis /schedule_task
commando dat een taak type accepteert (backup
, clear_cache
, of status_check
), de omgeving ID en een cron expressie.
/schedule_task backup 12345 0 0 * * *
Dit plant een dagelijkse backup om middernacht. Hier is de volledige opdrachtafhandeling:
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}`);
}
});
Geplande taken annuleren
Als er iets verandert of als de taak niet meer nodig is, kunnen gebruikers deze annuleren met:
/cancel_task
Hier is de implementatie:
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.`);
});
Alle geplande taken weergeven
Laten we gebruikers ook alle geplande taken laten zien:
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);
});
Dit geeft je Slackbot een heel nieuw niveau van autonomie. Backups, cache wissen en statuscontroles hoeven niet langer iemand van zijn werk te houden. Ze gebeuren gewoon stil, betrouwbaar en volgens schema.
Terugkerend onderhoud
Soms wil je een groep onderhoudstaken met regelmatige tussenpozen uitvoeren, zoals wekelijkse backups en cache wissen op zondagavond. Dat is waar onderhoudsvensters van pas kunnen komen.
Een onderhoudsvenster is een gepland tijdsblok waarin de bot automatisch vooraf gedefinieerde taken uitvoert, zoals:
- Een backup maken
- Cache wissen
- Het versturen van start- en voltooiingsmeldingen
Het format is eenvoudig:
/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]
Bijvoorbeeld:
/maintenance_window 12345 Sunday 2 3
Dit betekent dat er elke zondag om 2 uur ’s nachts 3 uur lang onderhoudstaken worden uitgevoerd. Hier is de volledige implementatie:
// 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}`);
}
});
Geautomatiseerde rapportage
Je wilt niet elke maandag wakker worden en je afvragen of er een backup is gemaakt van je WordPress site of dat hij urenlang down is geweest. Met geautomatiseerde rapportage kan je Slack bot jou en je team een snel prestatieoverzicht geven volgens een schema.
Dit soort rapportages zijn geweldig voor het bijhouden van zaken als:
- De huidige sitestatus
- Backup activiteit van de afgelopen 7 dagen
- PHP versie en primair domein
- Eventuele rode vlaggen, zoals geblokkeerde omgevingen of ontbrekende backups
Laten we een /schedule_report
commando maken dat dit automatiseert.
// 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}`
});
}
}
Foutafhandeling en monitoring
Zodra je bot echte bewerkingen gaat uitvoeren, zoals het wijzigen van omgevingen of het activeren van geplande taken, heb je meer nodig dan console.log()
om bij te houden wat er achter de schermen gebeurt.
Laten we dit opsplitsen in strakke, onderhoudbare lagen:
Gestructureerd loggen met Winston
In plaats van logs naar de console te printen, gebruik je winston
om gestructureerde logs naar bestanden te sturen, en optioneel naar diensten als Loggly of Datadog. Installeer het met het onderstaande commando:
npm install winston
Stel vervolgens logger.js
in :
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;
Vervang vervolgens in je app.js
alle calls console.log
of console.error
door:
const logger = require('./logger');
logger.info('Cache clear initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });
Waarschuwingen sturen naar admins via Slack
Je hebt de ADMIN_USERS
env variabele al, gebruik deze om je team direct op de hoogte te stellen in Slack wanneer er iets kritieks mislukt:
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 });
}
}
Gebruik het als volgt:
await alertAdmins('Backup Failed', {
environmentId,
error: error.message,
user: ``
});
Prestaties bijhouden met basic data
Ga niet voluit voor Prometheus als je alleen maar wilt zien hoe gezond je bot is. Houd een lichtgewicht prestatieobject bij:
const metrics = {
apiCalls: 0,
errors: 0,
commands: 0,
totalTime: 0,
get avgResponseTime() {
return this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
}
};
Werk dit bij in je kinstaRequest()
helper:
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;
}
Expose het via een commando als /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);
});
Optioneel: Herstelstappen definiëren
Als je herstellogica wilt implementeren (zoals het opnieuw proberen van cache wissen via SSH), maak dan een helper zoals:
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.' };
}
Houd het buiten de logica van je hoofdcommando tenzij het een kritiek pad is. In veel gevallen is het beter om de fout te loggen, beheerders te waarschuwen en mensen te laten beslissen wat te doen.
Je Slackbot implementeren en beheren
Zodra je bot compleet is, moet je hem uitrollen naar een productieomgeving waar hij 24/7 kan draaien.
Kinsta’s Sevalla is een uitstekende plek om dit soort bots te hosten. Het ondersteunt Node.js apps, omgevingsvariabelen, logging en schaalbare implementaties out of the box.
Je kunt je bot ook containeriseren met Docker of inzetten op elk cloudplatform dat Node.js en achtergronddiensten ondersteunt.
Hier zijn een paar dingen waar je aan moet denken voordat je live gaat:
- Gebruik omgevingsvariabelen voor alle secrets (Slack tokens, Kinsta API sleutels, SSH sleutels).
- Stel logging en uptime monitoring in zodat je weet wanneer er iets kapot gaat.
- Draai je bot met een procesmanager zoals PM2 of Docker’s
restart: always
policy om hem in leven te houden na crashes of herstarts. - Houd je SSH sleutels veilig, vooral als je ze gebruikt voor automatisering.
Samenvatting
Je hebt je Slackbot nu veranderd van een eenvoudige commando handler in een krachtige tool met echte interactiviteit, geplande automatisering en solide monitoring. Deze functies maken je bot nuttiger, betrouwbaarder en veel prettiger in gebruik, vooral voor teams die meerdere WordPress sites beheren.
En als je dat koppelt aan de kracht van de Kinsta API en de stressvrije hosting van Kinsta, dan heb je een setup die zowel schaalbaar als betrouwbaar is.