Os slackbots não precisam esperar que você digite comandos. Com a configuração correta, seu bot pode ajudar a gerenciar seus sites WordPress oferecendo botões interativos, menus suspensos, tarefas agendadas e alertas inteligentes – tudo isso diretamente no Slack.

Neste artigo, mostraremos a você como adicionar interatividade, automação e monitoramento ao seu slackbot.

Pré-requisitos

Antes de começar, certifique-se de ter:

  • Um aplicativo Slack com permissões de bot e um comando de barra.
  • Uma conta Kinsta com acesso à API e um site para você testar.
  • Node.js e NPM instalados localmente.
  • Familiaridade básica com JavaScript (ou, pelo menos, que você se sinta à vontade para copiar e ajustar o código).
  • Chaves API do Slack e da Kinsta.

Primeiros passos

Para criar esse Slackbot, o Node.js e a framework Bolt do Slack são usados para conectar comandos de barra que acionam ações via a API da Kinsta.

Não vamos repetir todas as etapas de criação de um aplicativo Slack ou de como obter acesso à API da Kinsta neste guia, pois isso já foi abordado no nosso guia anterior, Como criar um Slackbot com Node.js e API da Kinsta para Gerenciamento de Sites.

Se você ainda não leu esse guia, leia primeiro. Ele mostra como criar seu aplicativo Slack, obter o token do bot e o segredo de assinatura, além da chave API da Kinsta.

Adicione interatividade ao seu Slackbot

Slackbots não precisam se basear apenas em comandos slash. Com componentes interativos como botões, menus e modais, você pode transformar seu bot em uma ferramenta muito mais intuitiva e amigável ao usuário.

Em vez de digitar /clear_cache environment_id, imagine que você clique em um botão chamado Limpar cache logo após verificar o status de um site. Para fazer isso, você precisa do cliente da Web API do Slack. Instale-o em seu projeto com o comando abaixo:

npm install @slack/web-api

Em seguida, inicialize-o em seu site app.js:

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

Certifique-se de que SLACK_BOT_TOKEN esteja definido em seu arquivo .env. Agora, vamos aprimorar o comando /site_status do artigo anterior. Em vez de apenas enviar texto, anexamos botões para ações rápidas, como Limpar cache, Criar backup ou Verificar status detalhado.

Veja como fica o manipulador atualizado:

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

Cada clique em um botão aciona uma ação. Veja como lidamos com o botão Limpar 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}`);
  }
});

Você pode seguir o mesmo padrão para os botões de backup e status, apenas vinculando cada um ao endpoint da API ou à lógica de comando apropriada.

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

Use um menu suspenso para selecionar um site

Digitar IDs de ambiente não é divertido. E você espera que cada membro da equipe se lembre de qual ID pertence a qual ambiente? Isso não é realista.

Vamos tornar isso mais intuitivo. Em vez de pedir aos usuários que digitem /site_status [environment-id], daremos a eles um menu suspenso do Slack em que poderão escolher um site em uma lista. Assim que o site for selecionado, o bot exibirá o status e anexará os mesmos botões de ação rápida que implementamos anteriormente.

Para fazer isso, nós:

  • Buscamos todos os sites na API da Kinsta
  • Buscamos os ambientes de cada site
  • Criamos um menu suspenso com essas opções
  • Tratamos a seleção do usuário e exibimos o status do site

Aqui está o comando que mostra o menu suspenso:

app.command('/select_site', async ({ command, ack, say }) => {
  await ack();
  
  try {
    // Get all sites
    const response = await kinstaRequest('/sites');
    
    if (response && response.company && response.company.sites) {
      const sites = response.company.sites;
      
      // Create options for each site
      const options = [];
      
      for (const site of sites) {
        // Get environments for this site
        const envResponse = await kinstaRequest(`/sites/${site.id}/environments`);
        
        if (envResponse && envResponse.site && envResponse.site.environments) {
          for (const env of envResponse.site.environments) {
            options.push({
              text: {
                type: 'plain_text',
                text: `${site.name} (${env.name})`
              },
              value: env.id
            });
          }
        }
      }
      
      // Send message with dropdown
      await web.chat.postMessage({
        channel: command.channel_id,
        text: 'Select a site to manage:',
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: '*Select a site to manage:*'
            },
            accessory: {
              type: 'static_select',
              placeholder: {
                type: 'plain_text',
                text: 'Select a site'
              },
              options: options.slice(0, 100), // Slack has a limit of 100 options
              action_id: 'site_selected'
            }
          }
        ]
      });
    } else {
      await say('❌ Error retrieving sites. Please check your API credentials.');
    }
  } catch (error) {
    console.error('Error:', error);
    await say(`❌ Error retrieving sites: ${error.message}`);
  }
});

Quando um usuário escolhe um site, tratamos disso com este manipulador de ação:

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

Agora que nosso bot pode acionar ações com um botão e selecionar sites em uma lista, vamos nos certificar de que não executamos operações arriscadas acidentalmente.

Diálogos de confirmação

Algumas operações nunca devem ser executadas acidentalmente. Limpar um cache pode parecer inofensivo, mas se você estiver trabalhando em um site de produção, provavelmente não vai querer fazer isso com um único clique, especialmente se estiver apenas verificando o status do site. É aí que entram os modais (caixas de diálogo) do Slack.

Em vez de limpar imediatamente o cache quando você clica em clear_cache_button, mostramos um modal de confirmação. Veja como:

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

No código acima, usamos web.views.open() para iniciar um modal com um título claro, uma mensagem de aviso e dois botões – Limpar Cache e Cancelar – e armazenamos o environmentId em private_metadata para que você o tenha quando o usuário clicar em Clear Cache.

Quando o usuário clica no botão Limpar Cache no modal, o Slack envia um evento view_submission. Veja como lidar com ele e prosseguir com a operação real:

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

Neste código, depois que o usuário confirma, pegamos o environmentId de private_metadata, abrimos um DM privado usando web.conversations.open() para evitar a confusão de canais públicos, executamos a solicitação de API para limpar o cache e acompanhamos com uma mensagem de sucesso ou de erro, dependendo do resultado.

Indicadores de progresso

Alguns comandos do Slack são instantâneos, como limpar um cache ou verificar um status. Mas e outros? Nem tanto.

Criar um backup ou implantar arquivos pode levar vários segundos, ou até minutos. E se o seu bot simplesmente ficar em silêncio durante esse tempo, o usuário pode pensar que algo deu errado.

O Slack não oferece uma barra de progresso nativa, mas podemos simular uma com um pouco de criatividade. Aqui está uma função auxiliar que atualiza uma mensagem com uma barra de progresso visual usando o 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}``
        }
      }
    ]
  });
}

Vamos integrar isso em um comando /create_backup. Em vez de esperar que toda a operação seja concluída antes de responder, faremos o check-in com o usuário em cada etapa.

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

Notificações de sucesso/falha

Atualmente, seu bot provavelmente envia mensagens simples como ✅ Success ou ❌ Failed. Funciona, mas é sem graça — e não ajuda o usuário a entender por que algo deu certo ou o que fazer quando falha.

Vamos corrigir isso com a formatação adequada para mensagens de sucesso e de erro, juntamente com contexto útil, sugestões e formatação limpa.

Adicione esses utilitários ao seu site utils.js para que você possa reutilizá-los em todos os comandos:

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

Essas funções recebem dados estruturados e os transformam em markdown compatível com Slack, com emojis, etiquetas e quebras de linha. Muito mais fácil de visualizar no meio de um thread cheio no Slack. Veja como isso fica em um manipulador real de comandos. Vamos usar o /clear_cache como exemplo:

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

Automatize tarefas do WordPress com jobs agendados

Até agora, tudo o que o Slackbot faz acontece quando alguém aciona explicitamente um comando. Mas nem tudo deveria depender de alguém lembrar de rodar uma tarefa.

E se seu bot pudesse fazer backup dos seus sites automaticamente todas as noites? Ou verificar se algum site está fora do ar todas as manhãs antes da equipe acordar?

Vamos usar a biblioteca node-schedule para executar tarefas com base em expressões cron. Primeiro, instale-a:

npm install node-schedule

Agora, configure-a na parte superior do seu site app.js:

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

Também precisaremos de uma forma de rastrear os jobs agendados ativos, para que os usuários possam listá-los ou cancelá-los posteriormente:

const scheduledJobs = {};

Criando o comando de agendamento de tarefas

Começaremos com um comando básico /schedule_task que aceita um tipo de tarefa (backup, clear_cache ou status_check), o ID do ambiente e uma expressão cron.

/schedule_task backup 12345 0 0 * * *

Esse comando agendaria um backup diário à meia-noite. Aqui está o manipulador de comando completo:

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

Cancelando tarefas agendadas

Se algo mudar ou a tarefa não for mais necessária, os usuários podem cancelá-la com:

/cancel_task

Aqui está a implementação:

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

Listando todas as tarefas agendadas

Vamos permitir também que os usuários visualizem todos os jobs programados:

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

Isso dá ao seu Slackbot um nível totalmente novo de autonomia. Backups, limpezas de cache e verificações de status não precisam mais ser tarefas de alguém. Eles simplesmente acontecem de forma silenciosa, confiável e dentro do cronograma.

Manutenção recorrente

Às vezes, você quer executar um conjunto de tarefas de manutenção em intervalos regulares, como backups semanais e limpezas de cache nas noites de domingo. É aí que entram as janelas de manutenção (maintenance windows).

Uma janela de manutenção é um bloco de tempo programado em que o bot executa automaticamente tarefas predefinidas, como:

  • Criar um backup
  • Limpar o cache
  • Enviar notificações de início e conclusão

O formato é simples:

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

Por exemplo:

/maintenance_window 12345 Sunday 2 3

Isso significa que todo domingo, às 2h da manhã, as tarefas de manutenção são executadas por 3 horas. Aqui está a implementação completa:

// Add a command to create a maintenance window
app.command('/maintenance_window', async ({ command, ack, say }) => {
  await ack();
  
  // Expected format: environment_id day_of_week hour duration
  // Example: /maintenance_window 12345 Sunday 2 3
  const args = command.text.split(' ');
  
  if (args.length < 4) {
    await say('Please provide all required parameters. Usage: `/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]`');
    return;
  }
  
  const [environmentId, dayOfWeek, hour, duration] = args;
  
  // Validate inputs
  const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  if (!validDays.includes(dayOfWeek)) {
    await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
    return;
  }
  
  const hourInt = parseInt(hour, 10);
  if (isNaN(hourInt) || hourInt  23) {
    await say('Hour must be a number between 0 and 23.');
    return;
  }
  
  const durationInt = parseInt(duration, 10);
  if (isNaN(durationInt) || durationInt  12) {
    await say('Duration must be a number between 1 and 12 hours.');
    return;
  }
  
  // Convert day of week to cron format
  const dayMap = {
    'Sunday': 0,
    'Monday': 1,
    'Tuesday': 2,
    'Wednesday': 3,
    'Thursday': 4,
    'Friday': 5,
    'Saturday': 6
  };
  
  const cronDay = dayMap[dayOfWeek];
  
  // Create cron schedule for the start of the maintenance window
  const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
  
  // Generate a unique job ID
  const jobId = `maintenance_${environmentId}_${Date.now()}`;
  
  // Schedule the job
  try {
    const job = schedule.scheduleJob(cronSchedule, async function() {
      // Start of maintenance window
      await web.chat.postMessage({
        channel: command.channel_id,
        text: `🔧 *Maintenance Window Started*n*Environment:* `${environmentId}`n*Duration:* ${durationInt} hoursnnAutomatic maintenance tasks are now running.`
      });
      
      // Perform maintenance tasks
      try {
        // 1. Create a backup
        const backupResponse = await kinstaRequest(
          `/sites/environments/${environmentId}/manual-backups`,
          'POST',
          { tag: `Maintenance backup ${new Date().toISOString()}` }
        );
        
        if (backupResponse && backupResponse.operation_id) {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ Maintenance backup created. Operation ID: `${backupResponse.operation_id}``
          });
        }
        
        // 2. Clear cache
        const cacheResponse = await kinstaRequest(
          `/sites/environments/${environmentId}/clear-cache`,
          'POST'
        );
        
        if (cacheResponse && cacheResponse.operation_id) {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ Cache cleared. Operation ID: `${cacheResponse.operation_id}``
          });
        }
        
        // 3. Schedule end of maintenance window notification
        setTimeout(async () => {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ *Maintenance Window Completed*n*Environment:* `${environmentId}`nnAll maintenance tasks have been completed.`
          });
        }, durationInt * 60 * 60 * 1000); // Convert hours to milliseconds
      } catch (error) {
        console.error('Maintenance tasks error:', error);
        await web.chat.postMessage({
          channel: command.channel_id,
          text: `❌ Error during maintenance: ${error.message}`
        });
      }
    });
    
    // Store the job for later cancellation
    scheduledJobs[jobId] = {
      job,
      taskType: 'maintenance',
      environmentId,
      cronSchedule,
      dayOfWeek,
      hour: hourInt,
      duration: durationInt,
      userId: command.user_id,
      createdAt: new Date().toISOString()
    };
    
    await say(`✅ Maintenance window scheduled!
*Environment:* `${environmentId}`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00 for ${durationInt} hours
*Job ID:* `${jobId}`

To cancel this maintenance window, use `/cancel_task ${jobId}``);
  } catch (error) {
    console.error('Error scheduling maintenance window:', error);
    await say(`❌ Error scheduling maintenance window: ${error.message}`);
  }
});

Relatórios automatizados

Você não quer acordar toda segunda-feira se perguntando se o backup do seu site WordPress foi feito ou se ele ficou fora do ar por horas. Com os relatórios automatizados, seu bot do Slack pode fornecer a você e à sua equipe um resumo rápido do desempenho em uma programação.

Esse tipo de relatório é ótimo para que você mantenha o controle sobre coisas como:

  • O status atual do site
  • Atividade de backup nos últimos 7 dias
  • Versão do PHP e domínio principal
  • Quaisquer sinais de alerta, como ambientes bloqueados ou backups ausentes

Vamos criar um comando /schedule_report que automatize isso.

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

Tratamento de erros e monitoramento

Quando seu bot começar a realizar operações reais, como modificar ambientes ou acionar tarefas agendadas, você precisará de mais do que console.log() para acompanhar o que está acontecendo nos bastidores.

Vamos dividir isso em camadas limpas e de fácil manutenção:

Registros estruturados com Winston

Em vez de imprimir registros no console, use winston para enviar registros estruturados para arquivos e, opcionalmente, para serviços como Loggly ou Datadog. Instale-o com o comando abaixo:

npm install winston

Em seguida, configure 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;

No seu app.js, substitua console.log() ou console.error() por:

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

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

Envie alertas para administradores via Slack

Você já tem a variável de ambiente ADMIN_USERS, use-a para notificar sua equipe diretamente no Slack quando algo crítico falhar:

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

Use-a desta forma:

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

Monitore desempenho com métricas simples

Não use o Prometheus completo se você estiver apenas tentando verificar a integridade do seu bot. Mantenha um objeto de desempenho leve:

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

Atualize isso dentro do seu 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;
}

Exponha as métricas com o comando /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);
});

Opcional: defina etapas de recuperação

Se você quiser implementar a lógica de recuperação (como tentar novamente limpar o cache via SSH), basta criar um helper como:

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

Evite misturar esse tipo de lógica diretamente nos comandos principais, exceto quando for um caminho crítico. Em muitos casos, é melhor apenas registrar o erro, alertar os administradores e deixar que humanos tomem a decisão.

Implante e gerencie seu Slackbot

Quando o seu bot estiver completo, é hora de colocá-lo em produção, em um ambiente que esteja sempre disponível.

O Sevalla da Kinsta é um excelente lugar para você hospedar bots como esse. Ele oferece suporte a aplicativos Node.js, variáveis de ambiente, registro e implementações escalonáveis prontas para uso.

Como alternativa, você pode colocar seu bot em contêineres usando o Docker ou implantá-lo em qualquer plataforma de nuvem que ofereça suporte a Node.js e serviços em segundo plano.

Aqui estão algumas coisas que você deve ter em mente antes de entrar em operação:

  • Use variáveis de ambiente para todos os segredos (tokens do Slack, chaves API da Kinsta, chaves SSH).
  • Configure o registro e o monitoramento do tempo de atividade para saber quando algo falhar.
  • Execute seu bot com um gerenciador de processos como o PM2 ou a política restart: always do Docker para mantê-lo ativo após falhas ou reinicializações.
  • Mantenha suas chaves SSH seguras, especialmente se você as estiver usando para automação.

Resumo

Agora você transformou seu Slackbot de um simples manipulador de comandos em uma ferramenta poderosa com interatividade real, automação programada e monitoramento sólido. Esses recursos tornam seu bot mais útil, mais confiável e muito mais agradável de usar, especialmente para equipes que gerenciam vários sites WordPress.

E ao combinar tudo isso com o poder da API da Kinsta e a hospedagem sem estresse da Kinsta, você tem uma configuração que é escalável e confiável.

Joel Olawanle Kinsta

Joel é um desenvolvedor Frontend que trabalha na Kinsta como Editor Técnico. Ele é um professor apaixonado com amor pelo código aberto e já escreveu mais de 200 artigos técnicos, principalmente sobre JavaScript e seus frameworks.