Slackbots don’t have to attend so that you can sort out instructions. With the best setup, your bot can lend a hand arrange your WordPress websites by way of providing interactive buttons, dropdowns, scheduled duties, and good indicators — all proper within Slack.
On this article, we’ll display you the best way to upload interactivity, automation, and tracking on your Slack bot.
Must haves
Ahead of you get started, make sure to have:
- A Slack App with bot permissions and a slash command.
- A Kinsta account with API get right of entry to and a web page to check with.
- Node.js and NPM put in in the neighborhood.
- Elementary familiarity with JavaScript (or a minimum of relaxed copying and tweaking code).
- API keys for Slack and Kinsta.
Getting began
To construct this Slackbot, Node.js and Slack’s Bolt framework are used to cord up slash instructions that cause movements by the use of the Kinsta API.
We gained’t rehash each step of constructing a Slack app or getting Kinsta API get right of entry to on this information, as the ones are already coated in our previous information, Easy methods to Construct a Slackbot With Node.js and Kinsta API for Web site Control.
If you happen to haven’t observed that one but, learn it first. It walks you thru developing your Slack app, getting your bot token and signing secret, and getting your Kinsta API key.
Upload interactivity on your Slackbot
Slackbots don’t must depend on slash instructions on my own. With interactive parts like buttons, menus, and modals, you’ll be able to flip your bot right into a a lot more intuitive and user-friendly instrument.
As an alternative of typing /clear_cache environment_id
, consider clicking a button categorised Transparent Cache proper after checking a web page’s standing. To try this, you wish to have Slack’s Internet API consumer. Set up it into your challenge with the command under:
npm set up @slack/web-api
Then initialize it to your app.js
:
const { WebClient } = require('@slack/web-api');
const internet = new WebClient(procedure.env.SLACK_BOT_TOKEN);
Be sure that SLACK_BOT_TOKEN
is ready to your .env
record. Now, let’s beef up the /site_status
command from the former article. As an alternative of simply sending textual content, we connect buttons for speedy movements like Transparent Cache, Create Backup, or Take a look at Detailed Standing.
Right here’s what the up to date handler looks as if:
app.command('/site_status', async ({ command, ack, say }) => {
watch for ack();
const environmentId = command.textual content.trim();
if (!environmentId) {
watch for say('Please supply an atmosphere ID. Utilization: `/site_status [environment-id]`');
go back;
}
check out {
// Get surroundings standing
const reaction = watch for kinstaRequest(`/websites/environments/${environmentId}`);
if (reaction && reaction.web page && reaction.web page.environments && reaction.web page.environments.period > 0) {
const env = reaction.web page.environments[0];
// Structure the standing message
let statusMessage = formatSiteStatus(env);
// Ship message with interactive buttons
watch for internet.chat.postMessage({
channel: command.channel_id,
textual content: 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 {
watch for say(`⚠ No surroundings discovered with ID: `${environmentId}``);
}
} catch (error) {
console.error('Error checking web page standing:', error);
watch for say(`❌ Error checking web page standing: ${error.message}`);
}
});
Every button click on triggers an motion. Right here’s how we care for the Transparent Cache button:
// Upload motion handlers for the buttons
app.motion('clear_cache_button', async ({ frame, ack, reply }) => {
watch for ack();
const environmentId = frame.movements[0].worth;
watch for reply(`🔄 Clearing cache for surroundings `${environmentId}`...`);
check out {
// Name Kinsta API to clean cache
const reaction = watch for kinstaRequest(
`/websites/environments/${environmentId}/clear-cache`,
'POST'
);
if (reaction && reaction.operation_id) {
watch for reply(`✅ Cache clearing operation began! Operation ID: `${reaction.operation_id}``);
} else {
watch for reply('⚠ Cache clearing request used to be despatched, however no operation ID used to be returned.');
}
} catch (error) {
console.error('Cache clearing error:', error);
watch for reply(`❌ Error clearing cache: ${error.message}`);
}
});
You’ll observe the similar development for the backup and standing buttons, simply linking each and every one to the proper API endpoint or command common sense.
// Handlers for different buttons
app.motion('detailed_status_button', async ({ frame, ack, reply }) => {
watch for ack();
const environmentId = frame.movements[0].worth;
// Enforce detailed standing test very similar to the /detailed_status command
// ...
});
app.motion('create_backup_button', async ({ frame, ack, reply }) => {
watch for ack();
const environmentId = frame.movements[0].worth;
// Enforce backup introduction very similar to the /create_backup command
// ...
});
Use a dropdown to make a choice a web page
Typing surroundings IDs isn’t amusing. And anticipating each workforce member to bear in mind which ID belongs to which surroundings? That’s now not life like.
Let’s make this extra intuitive. As an alternative of asking customers to sort /site_status [environment-id]
, we’ll give them a Slack dropdown the place they may be able to select a web page from an inventory. When they choose one, the bot will display the standing and connect the similar quick-action buttons we applied previous.
To try this, we:
- Fetch all websites from the Kinsta API
- Fetch the environments for each and every web page
- Construct a dropdown menu with those choices
- Care for the person’s variety and show the web page’s standing
Right here’s the command that displays the dropdown:
app.command('/select_site', async ({ command, ack, say }) => {
watch for ack();
check out {
// Get all websites
const reaction = watch for kinstaRequest('/websites');
if (reaction && reaction.corporate && reaction.corporate.websites) {
const websites = reaction.corporate.websites;
// Create choices for each and every web page
const choices = [];
for (const web page of web sites) {
// Get environments for this web page
const envResponse = watch for kinstaRequest(`/websites/${web page.identity}/environments`);
if (envResponse && envResponse.web page && envResponse.web page.environments) {
for (const env of envResponse.web page.environments) {
choices.push({
textual content: {
sort: 'plain_text',
textual content: `${web page.identify} (${env.identify})`
},
worth: env.identity
});
}
}
}
// Ship message with dropdown
watch for internet.chat.postMessage({
channel: command.channel_id,
textual content: 'Choose a web page to control:',
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 {
watch for say('❌ Error retrieving websites. Please test your API credentials.');
}
} catch (error) {
console.error('Error:', error);
watch for say(`❌ Error retrieving websites: ${error.message}`);
}
});
When a person alternatives a web page, we care for that with this motion handler:
// Care for the web page variety
app.motion('site_selected', async ({ frame, ack, reply }) => {
watch for ack();
const environmentId = frame.movements[0].selected_option.worth;
const siteName = frame.movements[0].selected_option.textual content.textual content;
// Get surroundings standing
check out {
const reaction = watch for kinstaRequest(`/websites/environments/${environmentId}`);
if (reaction && reaction.web page && reaction.web page.environments && reaction.web page.environments.period > 0) {
const env = reaction.web page.environments[0];
// Structure the standing message
let statusMessage = `*${siteName}* (ID: `${environmentId}`)nn${formatSiteStatus(env)}`;
// Ship message with interactive buttons (very similar to the site_status command)
// ...
} else {
watch for reply(`⚠ No surroundings discovered with ID: `${environmentId}``);
}
} catch (error) {
console.error('Error:', error);
watch for reply(`❌ Error retrieving surroundings: ${error.message}`);
}
});
Now that our bot can cause movements with a button and choose websites from an inventory, let’s be sure that we don’t by accident run dangerous operations.
Affirmation dialogs
Some operations must by no means run by accident. Clearing a cache may sound innocuous, however for those who’re running on a manufacturing web page, you almost certainly don’t wish to do it with a unmarried click on — particularly for those who had been simply checking the web page standing. That’s the place Slack modals (dialogs) are available.
As an alternative of instantly clearing the cache when the clear_cache_button
is clicked, we display a affirmation modal. Right here’s how:
app.motion('clear_cache_button', async ({ frame, ack, context }) => {
watch for ack();
const environmentId = frame.movements[0].worth;
// Open a affirmation conversation
check out {
watch for internet.perspectives.open({
trigger_id: frame.trigger_id,
view: {
sort: 'modal',
callback_id: 'clear_cache_confirmation',
private_metadata: environmentId,
identify: {
sort: 'plain_text',
textual content: 'Ascertain Cache Clearing'
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Are you sure you want to clear the cache for environment `${environmentId}`?`
}
}
],
put up: {
sort: 'plain_text',
textual content: 'Transparent Cache'
},
shut: {
sort: 'plain_text',
textual content: 'Cancel'
}
}
});
} catch (error) {
console.error('Error opening affirmation conversation:', error);
}
});
Within the code above, we use internet.perspectives.open()
to release a modal with a transparent identify, a caution message, and two buttons — Transparent Cache and Cancel — and retailer the environmentId
in private_metadata
so we’ve it when the person clicks Transparent Cache.
As soon as the person clicks the Transparent Cache button within the modal, Slack sends a view_submission
tournament. Right here’s the best way to care for it and continue with the true operation:
// Care for the affirmation conversation submission
app.view('clear_cache_confirmation', async ({ ack, frame, view }) => {
watch for ack();
const environmentId = view.private_metadata;
const userId = frame.person.identity;
// Discover a DM channel with the person to answer
const outcome = watch for internet.conversations.open({
customers: userId
});
const channel = outcome.channel.identity;
watch for internet.chat.postMessage({
channel,
textual content: `🔄 Clearing cache for surroundings `${environmentId}`...`
});
check out {
// Name Kinsta API to clean cache
const reaction = watch for kinstaRequest(
`/websites/environments/${environmentId}/clear-cache`,
'POST'
);
if (reaction && reaction.operation_id) {
watch for internet.chat.postMessage({
channel,
textual content: `✅ Cache clearing operation began! Operation ID: `${reaction.operation_id}``
});
} else {
watch for internet.chat.postMessage({
channel,
textual content: '⚠ Cache clearing request used to be despatched, however no operation ID used to be returned.'
});
}
} catch (error) {
console.error('Cache clearing error:', error);
watch for internet.chat.postMessage({
channel,
textual content: `❌ Error clearing cache: ${error.message}`
});
}
});
On this code, after the person confirms, we take hold of the environmentId
from private_metadata
, open a personal DM the usage of internet.conversations.open()
to keep away from cluttering public channels, run the API request to clean the cache, and observe up with a luck or error message relying at the outcome.
Development signs
Some Slack instructions are fast, similar to clearing a cache or checking a standing. However others? Now not such a lot.
Making a backup or deploying information can take a number of seconds and even mins. And in case your bot simply sits there silent all through that point, customers may suppose one thing broke.
Slack doesn’t provide you with a local development bar, however we will faux one with a bit of creativity. Right here’s a helper serve as that updates a message with a visible development bar the usage of block equipment:
async serve as updateProgress(channel, messageTs, textual content, share) {
// Create a development bar
const barLength = 20;
const filledLength = Math.spherical(barLength * (share / 100));
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
watch for internet.chat.replace({
channel,
ts: messageTs,
textual content: `${textual content} [${percentage}%]`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${text} [${percentage}%]n`${bar}``
}
}
]
});
}
Let’s combine this right into a /create_backup
command. As an alternative of looking ahead to the entire operation to finish sooner than replying, we’ll test in with the person at each and every step.
app.command('/create_backup', async ({ command, ack, say }) => {
watch for ack();
const args = command.textual content.cut up(' ');
const environmentId = args[0];
const tag = args.period > 1 ? args.slice(1).sign up for(' ') : `Handbook backup ${new Date().toISOString()}`;
if (!environmentId) {
watch for say('Please supply an atmosphere ID. Utilization: `/create_backup [environment-id] [optional-tag]`');
go back;
}
// Put up preliminary message and get its timestamp for updates
const preliminary = watch for say('🔄 Beginning backup...');
const messageTs = preliminary.ts;
check out {
// Replace development to ten%
watch for updateProgress(command.channel_id, messageTs, '🔄 Growing backup...', 10);
// Name Kinsta API to create a backup
const reaction = watch for kinstaRequest(
`/websites/environments/${environmentId}/manual-backups`,
'POST',
{ tag }
);
if (reaction && reaction.operation_id) {
watch for updateProgress(command.channel_id, messageTs, '🔄 Backup in development...', 30);
// Ballot the operation standing
let finished = false;
let share = 30;
whilst (!finished && share setTimeout(unravel, 3000));
// Take a look at operation standing
const statusResponse = watch for kinstaRequest(`/operations/${reaction.operation_id}`);
if (statusResponse && statusResponse.operation) {
const operation = statusResponse.operation;
if (operation.standing === 'finished') {
finished = true;
share = 100;
} else if (operation.standing === 'failed') {
watch for internet.chat.replace({
channel: command.channel_id,
ts: messageTs,
textual content: `❌ Backup failed! Error: $`
});
go back;
} else {
// Increment development
share += 10;
if (share > 95) share = 95;
watch for updateProgress(
command.channel_id,
messageTs,
'🔄 Backup in development...',
share
);
}
}
}
// Ultimate replace
watch for internet.chat.replace({
channel: command.channel_id,
ts: messageTs,
textual content: `✅ Backup finished effectively!`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `✅ Backup completed successfully!n*Tag:* ${tag}n*Operation ID:* `${response.operation_id}``
}
}
]
});
} else {
watch for internet.chat.replace({
channel: command.channel_id,
ts: messageTs,
textual content: '⚠ Backup request used to be despatched, however no operation ID used to be returned.'
});
}
} catch (error) {
console.error('Backup introduction error:', error);
watch for internet.chat.replace({
channel: command.channel_id,
ts: messageTs,
textual content: `❌ Error developing backup: ${error.message}`
});
}
});
Good fortune/failure notifications
Presently, your bot most probably sends again undeniable textual content like
Let’s repair that with correct formatting for luck and blunder messages along helpful context, ideas, and blank formatting.
Upload those utilities on your utils.js
so you’ll be able to reuse them throughout all instructions:
serve as formatSuccessMessage(identify, main points = []) {
let message = `✅ *${identify}*nn`;
if (main points.period > 0) {
main points.forEach(element => {
message += `• ${element.label}: ${element.worth}n`;
});
}
go back message;
}
serve as formatErrorMessage(identify, error, ideas = []) {
let message = `❌ *${identify}*nn`;
message += `*Error:* ${error}nn`;
if (ideas.period > 0) {
message += '*Ideas:*n';
ideas.forEach(advice => {
message += `• ${advice}n`;
});
}
go back message;
}
module.exports = {
connectToSite,
logCommand,
formatSuccessMessage,
formatErrorMessage
};
Those purposes take structured enter and switch it into Slack-friendly markdown with emoji, labels, and line breaks. A lot more uncomplicated to scan in the course of a hectic Slack thread. Right here’s what that appears like within an actual command handler. Let’s use /clear_cache
as the instance:
app.command('/clear_cache', async ({ command, ack, say }) => {
watch for ack();
const environmentId = command.textual content.trim();
if (!environmentId) {
watch for say('Please supply an atmosphere ID. Utilization: `/clear_cache [environment-id]`');
go back;
}
check out {
watch for say('🔄 Processing...');
// Name Kinsta API to clean cache
const reaction = watch for kinstaRequest(
`/websites/environments/${environmentId}/clear-cache`,
'POST'
);
if (reaction && reaction.operation_id) {
const { formatSuccessMessage } = require('./utils');
watch for say(formatSuccessMessage('Cache Clearing Began', [
{ label: 'Environment ID', value: ``${environmentId}`` },
{ label: 'Operation ID', value: ``${response.operation_id}`` },
{ label: 'Status', value: 'In Progress' }
]));
} else {
const { formatErrorMessage } = require('./utils');
watch for 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');
watch for say(formatErrorMessage(
'Cache Clearing Error',
error.message,
[
'Check your environment ID',
'Verify your API credentials',
'Try again later'
]
));
}
});
Automate WordPress duties with scheduled jobs
To this point, the whole thing your Slackbot does occurs when anyone explicitly triggers a command. However now not the whole thing must rely on anyone remembering to run it.
What in case your bot may routinely again up your websites each night time? Or test if any web page is down each morning sooner than the workforce wakes up.
We’ll use the node-schedule library to run duties according to cron expressions. First, set up it:
npm set up node-schedule
Now, set it up on the best of your app.js
:
const agenda = require('node-schedule');
We’ll additionally desire a strategy to observe lively scheduled jobs so customers can checklist or cancel them later:
const scheduledJobs = {};
Growing the agenda activity command
We’ll get started with a elementary /schedule_task
command that accepts a job sort (backup
, clear_cache
, or status_check
), the surroundings ID, and a cron expression.
/schedule_task backup 12345 0 0 * * *
This might agenda a day by day backup at nighttime. Right here’s the total command handler:
app.command('/schedule_task', async ({ command, ack, say }) => {
watch for ack();
const args = command.textual content.cut up(' ');
if (args.period {
console.log(`Working scheduled ${taskType} for surroundings ${environmentId}`);
check out {
transfer (taskType) {
case 'backup':
watch for kinstaRequest(`/websites/environments/${environmentId}/manual-backups`, 'POST', {
tag: `Scheduled backup ${new Date().toISOString()}`
});
ruin;
case 'clear_cache':
watch for kinstaRequest(`/websites/environments/${environmentId}/clear-cache`, 'POST');
ruin;
case 'status_check':
const reaction = watch for kinstaRequest(`/websites/environments/${environmentId}`);
const env = reaction?.web page?.environments?.[0];
if (env) {
console.log(`Standing: ${env.display_name} is ${env.is_blocked ? 'blocked' : 'operating'}`);
}
ruin;
}
} catch (err) {
console.error(`Scheduled ${taskType} failed for ${environmentId}:`, err.message);
}
});
scheduledJobs[jobId] = {
activity,
taskType,
environmentId,
cronSchedule,
userId: command.user_id,
createdAt: new Date().toISOString()
};
watch for say(`✅ Scheduled activity created!
*Activity:* ${taskType}
*Setting:* `${environmentId}`
*Cron:* `${cronSchedule}`
*Activity ID:* `${jobId}`
To cancel this activity, run `/cancel_task ${jobId}``);
} catch (err) {
console.error('Error developing scheduled activity:', err);
watch for say(`❌ Did not create scheduled activity: ${err.message}`);
}
});
Cancelling scheduled duties
If one thing adjustments or the duty isn’t wanted anymore, customers can cancel it with:
/cancel_task
Right here’s the implementation:
app.command('/cancel_task', async ({ command, ack, say }) => {
watch for ack();
const jobId = command.textual content.trim();
if (!scheduledJobs[jobId]) {
watch for say(`⚠ No activity discovered with ID: `${jobId}``);
go back;
}
scheduledJobs[jobId].activity.cancel();
delete scheduledJobs[jobId];
watch for say(`✅ Activity `${jobId}` has been cancelled.`);
});
Checklist all scheduled duties
Let’s additionally let customers view all of the jobs which have been scheduled:
app.command('/list_tasks', async ({ command, ack, say }) => {
watch for ack();
const duties = Object.entries(scheduledJobs);
if (duties.period === 0) {
watch for say('No scheduled duties discovered.');
go back;
}
let message = '*Scheduled Duties:*nn';
for (const [jobId, job] of duties) {
message += `• *Activity ID:* `${jobId}`n`;
message += ` - Activity: ${activity.taskType}n`;
message += ` - Setting: `${activity.environmentId}`n`;
message += ` - Cron: `${activity.cronSchedule}`n`;
message += ` - Created by way of: nn`;
}
message += '_Use `/cancel_task [job_id]` to cancel a job._';
watch for say(message);
});
This offers your Slackbot an entire new stage of autonomy. Backups, cache clears, and standing tests not must be anyone’s activity. They only occur quietly, reliably, and on agenda.
Routine upkeep
Every now and then, you need to run a bunch of upkeep duties at common periods, like weekly backups and cache clears on Sunday nights. That’s the place upkeep home windows are available.
A upkeep window is a scheduled block of time when the bot routinely runs predefined duties like:
- Making a backup
- Clearing the cache
- Sending get started and final touch notifications
The structure is inconspicuous:
/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]
For instance:
/maintenance_window 12345 Sunday 2 3
Which means each Sunday at 2 AM, upkeep duties are run for three hours. Right here’s the total implementation:
// Upload a command to create a upkeep window
app.command('/maintenance_window', async ({ command, ack, say }) => {
watch for ack();
// Anticipated structure: environment_id day_of_week hour period
// Instance: /maintenance_window 12345 Sunday 2 3
const args = command.textual content.cut up(' ');
if (args.period < 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 () => {
watch for internet.chat.postMessage({
channel: command.channel_id,
textual content: `✅ *Upkeep Window Finished*n*Setting:* `${environmentId}`nnAll upkeep duties were finished.`
});
}, durationInt * 60 * 60 * 1000); // Convert hours to milliseconds
} catch (error) {
console.error('Upkeep duties error:', error);
watch for internet.chat.postMessage({
channel: command.channel_id,
textual content: `❌ Error all through upkeep: ${error.message}`
});
}
});
// Retailer the activity for later cancellation
scheduledJobs[jobId] = {
activity,
taskType: 'upkeep',
environmentId,
cronSchedule,
dayOfWeek,
hour: hourInt,
period: durationInt,
userId: command.user_id,
createdAt: new Date().toISOString()
};
watch for say(`✅ Upkeep window scheduled!
*Setting:* `${environmentId}`
*Agenda:* Each ${dayOfWeek} at ${hourInt}:00 for ${durationInt} hours
*Activity ID:* `${jobId}`
To cancel this upkeep window, use `/cancel_task ${jobId}``);
} catch (error) {
console.error('Error scheduling upkeep window:', error);
watch for say(`❌ Error scheduling upkeep window: ${error.message}`);
}
});
Computerized reporting
You don’t wish to get up each Monday questioning in case your WordPress web page used to be subsidized up or if it’s been down for hours. With automatic reporting, your Slack bot can provide you with and your workforce a snappy efficiency abstract on a agenda.
This type of record is superb for retaining tabs on such things as:
- The present web page standing
- Backup task during the last 7 days
- PHP model and number one area
- Any pink flags, like blocked environments or lacking backups
Let’s construct a /schedule_report
command that automates this.
// Upload a command to agenda weekly reporting
app.command('/schedule_report', async ({ command, ack, say }) => {
watch for ack();
// Anticipated structure: environment_id day_of_week hour
// Instance: /schedule_report 12345 Monday 9
const args = command.textual content.cut up(' ');
if (args.period < 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);
go back backupDate >= oneWeekAgo;
});
backupsCount = recentBackups.period;
if (recentBackups.period > 0) {
latestBackup = recentBackups.kind((a, b) => b.created_at - a.created_at)[0];
}
}
// Get surroundings standing
const statusEmoji = env.is_blocked ? '🔴' : '🟢';
const statusText = env.is_blocked ? 'Blocked' : 'Working';
// Create record message
const reportDate = new Date().toLocaleDateString('en-US', {
weekday: 'lengthy',
12 months: 'numeric',
month: 'lengthy',
day: 'numeric'
});
const reportMessage = `📊 *Weekly Document - ${reportDate}*
*Web site:* ${env.display_name}
*Setting ID:* `${environmentId}`
*Standing Abstract:*
• Present Standing: ${statusEmoji} ${statusText}
• PHP Model: $ 'Unknown'
• Number one Area: $
*Backup Abstract:*
• Overall Backups (Final 7 Days): ${backupsCount}
• Newest Backup: ${latestBackup ? new Date(latestBackup.created_at).toLocaleString() : 'N/A'}
• Newest Backup Kind: ${latestBackup ? latestBackup.sort : 'N/A'}
*Suggestions:*
• ${backupsCount === 0 ? '⚠ No fresh backups discovered. Believe making a handbook backup.' : '✅ Common backups are being created.'}
• ${env.is_blocked ? '⚠ Web site is recently blocked. Take a look at for problems.' : '✅ Web site is operating generally.'}
_This is an automatic record. For detailed knowledge, use the `/site_status ${environmentId}` command._`;
watch for internet.chat.postMessage({
channel: channelId,
textual content: reportMessage
});
} catch (error) {
console.error('Document era error:', error);
watch for internet.chat.postMessage({
channel: channelId,
textual content: `❌ Error producing weekly record: ${error.message}`
});
}
}
Error dealing with and tracking
As soon as your bot begins acting actual operations like editing environments or triggering scheduled duties, you wish to have greater than console.log()
to stay observe of what’s taking place at the back of the scenes.
Let’s ruin this down into blank, maintainable layers:
Structured logging with Winston
As an alternative of printing logs to the console, use winston
to ship structured logs to information, and optionally to products and services like Loggly or Datadog. Set up it with the command under:
npm set up winston
Subsequent, arrange logger.js
:
const winston = require('winston');
const fs = require('fs');
const trail = require('trail');
const logsDir = trail.sign up for(__dirname, '../logs');
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir);
const logger = winston.createLogger({
stage: 'information',
structure: winston.structure.mix(
winston.structure.timestamp(),
winston.structure.json()
),
defaultMeta: { carrier: '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;
Then, to your app.js
, change out any console.log
or console.error
calls with:
const logger = require('./logger');
logger.information('Cache clean initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });
Ship indicators to admins by the use of Slack
You have already got the ADMIN_USERS
env variable, use it to inform your workforce without delay in Slack when one thing vital fails:
async serve as alertAdmins(message, metadata = {}) {
for (const userId of ADMIN_USERS) {
const dm = watch for internet.conversations.open({ customers: userId });
const channel = dm.channel.identity;
let alert = `🚨 *${message}*n`;
for (const [key, value] of Object.entries(metadata)) {
alert += `• *${key}:* ${worth}n`;
}
watch for internet.chat.postMessage({ channel, textual content: alert });
}
}
Use it like this:
watch for alertAdmins('Backup Failed', {
environmentId,
error: error.message,
person: ``
});
Monitor efficiency with elementary metrics
Don’t cross complete Prometheus for those who’re simply seeking to see how wholesome your bot is. Stay a light-weight efficiency object:
const metrics = {
apiCalls: 0,
mistakes: 0,
instructions: 0,
totalTime: 0,
get avgResponseTime() {
go back this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
}
};
Replace this within your kinstaRequest()
helper:
const get started = Date.now();
check out {
metrics.apiCalls++;
const res = watch for fetch(...);
go back watch for res.json();
} catch (err) {
metrics.mistakes++;
throw err;
} after all {
metrics.totalTime += Date.now() - get started;
}
Reveal it by the use of a command like /bot_performance
:
app.command('/bot_performance', async ({ command, ack, say }) => {
watch for ack();
if (!ADMIN_USERS.comprises(command.user_id)) {
go back watch for say('⛔ Now not approved.');
}
const msg = `📊 *Bot Metrics*
• API Calls: ${metrics.apiCalls}
• Mistakes: ${metrics.mistakes}
• Avg Reaction Time: ${metrics.avgResponseTime.toFixed(2)}ms
• Instructions Run: ${metrics.instructions}`;
watch for say(msg);
});
Non-compulsory: Outline restoration steps
If you wish to put in force restoration common sense (like retrying cache clears by the use of SSH), simply create a helper like:
async serve as attemptRecovery(environmentId, factor) {
logger.warn('Making an attempt restoration', { environmentId, factor });
if (factor === 'cache_clear_failure') {
// fallback common sense right here
}
// Go back a restoration standing object
go back { luck: true, message: 'Fallback ran.' };
}
Stay it from your major command common sense until it’s a vital trail. In lots of instances, it’s higher to log the mistake, alert admins, and let people make a decision what to do.
Deploy and arrange your Slackbot
As soon as your bot is feature-complete, you must deploy it to a manufacturing surroundings the place it may well run 24/7.
Kinsta’s Sevalla is a superb position to host bots like this. It helps Node.js apps, surroundings variables, logging, and scalable deployments out of the field.
However, you’ll be able to containerize your bot the usage of Docker or deploy it to any cloud platform that helps Node.js and background products and services.
Right here are some things to bear in mind sooner than going are living:
- Use surroundings variables for all secrets and techniques (Slack tokens, Kinsta API keys, SSH keys).
- Arrange logging and uptime tracking so you understand when one thing breaks.
- Run your bot with a procedure supervisor like PM2 or Docker’s
restart: all the time
coverage to stay it alive after crashes or restarts. - Stay your SSH keys safe, particularly for those who’re the usage of them for automation.
Abstract
You’ve now taken your Slackbot from a easy command handler to an impressive instrument with actual interactivity, scheduled automation, and forged tracking. Those options make your bot extra helpful, extra dependable, and far more delightful to make use of, particularly for groups managing a couple of WordPress websites.
And whilst you pair that with the facility of the Kinsta API and enjoyable webhosting from Kinsta, you’ve were given a setup that’s each scalable and loyal.
The put up Enforce interactivity, scheduling, and tracking in Slackbots for managing WordPress websites seemed first on Kinsta®.
WP Hosting