diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c2c8e44 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "standard" + ], + "rules": { + "no-multi-spaces": [ + "error", + { + "ignoreEOLComments": true + } + ] + } +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..371aa87 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Node: Nodemon", + "processId": "${command:PickProcess}", + "restart": true, + "protocol": "inspector", + }, + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 5eee56a..2974c57 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This application has a few entry points and features to help you keep track of r | Name and Description | Visual | |------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **1. Scheduled Reminders**
You can invite the bot to public channels and it will monitor for and remind the channel about messages that fit specific criteria on a scheduled basis.

All messages, except those posted by the app are counted, so this works great with [Slack's Workflow Builder](https://slack.com/slack-tips/quickly-field-requests-for-your-team) and any other monitoring integration that you use that uses the emojis you configure. | [![](docs/assets/func_1_scheduled_reminders.png)](docs/assets/func_1_scheduled_reminders.png) | +| **1. Scheduled Jobs (aka Reminders)**
You can invite the bot to public channels and it will monitor for and remind the channel about messages that fit specific criteria on a scheduled basis.

All messages, except those posted by the app are counted, so this works great with [Slack's Workflow Builder](https://slack.com/slack-tips/quickly-field-requests-for-your-team) and any other monitoring integration that you use that uses the emojis you configure. | [![](docs/assets/func_1_scheduled_jobs.png)](docs/assets/func_1_scheduled_jobs.png) | | **2. Ad-hoc Reporting**
You can use the global shortcut :zap: to create ad-hoc reports on any public channel. It'll give you top-line message counts by urgency and status and provide a CSV for offline analysis too. 
  1. Trigger the modal with a [global shortcut](https://slackhq.com/speed-up-work-with-apps-for-slack) and configure your report in the resulting modal
  2. Triage stats bot will be added to the specified channel and run its analysis
  3. Triage stats will be delivered to you in a DM from the bot
| [![](docs/assets/func_2_ad_hoc_reports.gif)](docs/assets/func_2_ad_hoc_reports.gif) | | **3. View Configuration**
The app's [Slack App Home](https://api.slack.com/surfaces/tabs) offers users a view into the configuration of the application | [![](docs/assets/func_3_app_home.png)](docs/assets/func_3_app_home.png) | diff --git a/app.js b/app.js index 236114d..611ab3f 100644 --- a/app.js +++ b/app.js @@ -18,7 +18,7 @@ const triageConfig = require('./config') const modalViews = require('./views/modals.blockkit') const appHomeView = require('./views/app_home.blockkit') const { getAllMessagesForPastHours, filterAndEnrichMessages, messagesToCsv } = require('./helpers/messages') -const { scheduleReminders, manuallyTriggerScheduledJobs } = require('./helpers/scheduled_jobs') +const { scheduleJobs, manuallyTriggerScheduledJobs } = require('./helpers/scheduled_jobs') // ==================================== // === Initialization/Configuration === @@ -52,7 +52,7 @@ const app = new App({ let teamData = installation.team teamData = Object.assign(teamData, installation) delete teamData.team // we already have this information from the assign above - delete teamData.user.token // we dont want a user token, if the scopes are requested + delete teamData.user.token // we don't want a user token, if the scopes are requested // Do an upsert so that we always have just one document per team ID await AuthedTeam.findOneAndUpdate({ id: teamData.id }, teamData, { upsert: true }) @@ -72,7 +72,7 @@ const app = new App({ // ========================================================================= // Handle the shortcut we configured in the Slack App Config -app.shortcut('triage_stats', async ({ ack, context, body }) => { +app.shortcut('channel_stats', async ({ ack, context, body }) => { // Acknowledge right away await ack() @@ -80,11 +80,11 @@ app.shortcut('triage_stats', async ({ ack, context, body }) => { await app.client.views.open({ token: context.botToken, trigger_id: body.trigger_id, - view: modalViews.select_triage_channel + view: modalViews.select_channel_and_config }) }) -// Handle `view_submision` of modal we opened as a result of the `triage_stats` shortcut +// Handle `view_submision` of modal we opened as a result of the `channel_stats` shortcut app.view('channel_selected', async ({ body, view, ack, client, logger, context }) => { // Acknowledge right away await ack() @@ -94,9 +94,11 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context } view.state.values.channel.channel.selected_conversation const nHoursToGoBack = parseInt(view.state.values.n_hours.n_hours.selected_option.value) || 7 + const statsType = + view.state.values.stats_type.stats_type.selected_option.value try { - // Get converstion info; this will throw an error if the bot does not have access to it + // Get conversation info; this will throw an error if the bot does not have access to it const conversationInfo = await client.conversations.info({ channel: selectedChannelId, include_num_members: true @@ -110,7 +112,7 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context } // Let the user know, in a DM from the bot, that we're working on their request const msgWorkingOnIt = await client.chat.postMessage({ channel: submittedByUserId, - text: `*You asked for triage stats for <#${selectedChannelId}>*.\n` + + text: `*You asked for _${statsType} stats_ for <#${selectedChannelId}>*.\n` + `I'll work on the stats for the past ${nHoursToGoBack} hours right away!` }) @@ -118,10 +120,10 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context } await client.chat.postMessage({ channel: msgWorkingOnIt.channel, thread_ts: msgWorkingOnIt.ts, - text: `A number for you while you wait.. the channel has ${conversationInfo.channel.num_members} members (including apps) currently` + text: `A number for you while you wait.. <#${selectedChannelId}> has ${conversationInfo.channel.num_members} members (including apps) currently` }) - // Get all messages from the beginning of time (probably not a good idea) + // Get all messages for the time period specified const allMessages = await getAllMessagesForPastHours( selectedChannelId, nHoursToGoBack, @@ -129,60 +131,62 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context } ) // Use a helper method to enrich the messages we have - const allMessagesEnriched = filterAndEnrichMessages(allMessages, selectedChannelId, context.botId) - - // For each level, let's do some analysis! - const levelDetailBlocks = [] - for (const i in triageConfig._.levels) { - const level = triageConfig._.levels[i] - const allMessagesForLevel = allMessagesEnriched.filter( - m => m[`_level_${level}`] === true - ) - - // Formulate strings for each status - const countsStrings = triageConfig._.statuses.map(status => { - const messagesForLevelAndStatus = allMessagesForLevel.filter( - m => m[`_status_${status}`] === true + const allMessagesEnriched = filterAndEnrichMessages(allMessages, selectedChannelId, context.botId, statsType) + + if (statsType === 'triage') { + // For each level, let's do some analysis! + const levelDetailBlocks = [] + for (const i in triageConfig._.levels) { + const level = triageConfig._.levels[i] + const allMessagesForLevel = allMessagesEnriched.filter( + m => m[`_level_${level}`] === true ) - return `\tMessages ${status} ${triageConfig._.statusToEmoji[status]}: ${messagesForLevelAndStatus.length}` - }) - // Add level block to array - levelDetailBlocks.push( - { + // Formulate strings for each status + const countsStrings = triageConfig._.statuses.map(status => { + const messagesForLevelAndStatus = allMessagesForLevel.filter( + m => m[`_status_${status}`] === true + ) + return `\tMessages ${status} ${triageConfig._.statusToEmoji[status]}: ${messagesForLevelAndStatus.length}` + }) + + // Add level block to array + levelDetailBlocks.push( + { + type: 'section', + text: { + type: 'mrkdwn', + text: `${triageConfig._.levelToEmoji[level]} *${level}* (${allMessagesForLevel.length} total)\n${countsStrings.join('\n')}` + } + } + ) + } + + // Send a single message to the thread with all of the stats by level + await client.chat.postMessage({ + channel: msgWorkingOnIt.channel, + thread_ts: msgWorkingOnIt.ts, + blocks: [{ type: 'section', text: { type: 'mrkdwn', - text: `${triageConfig._.levelToEmoji[level]} *${level}* (${allMessagesForLevel.length} total)\n${countsStrings.join('\n')}` + text: "Here's a summary of the messages needing attention by urgency level and status:" } - } - ) + }].concat(levelDetailBlocks) + }) } - // Send a single message to the thread with all of the stats by level - await client.chat.postMessage({ - channel: msgWorkingOnIt.channel, - thread_ts: msgWorkingOnIt.ts, - blocks: [{ - type: 'section', - text: { - type: 'mrkdwn', - text: "Here's a summary of the messages needing attention by urgency level and status:" - } - }].concat(levelDetailBlocks) - }) - // Try to parse our object to CSV and upload it as an attachment try { // Convert object to CSV - const csvString = messagesToCsv(allMessagesEnriched) + const csvString = messagesToCsv(allMessagesEnriched, statsType) // Upload CSV File await client.files.upload({ channels: msgWorkingOnIt.channel, content: csvString, title: `All messages from the past ${nHoursToGoBack} hours`, - filename: 'allMessages.csv', + filename: `${selectedChannelId}_last${nHoursToGoBack}hours_allMessages_${statsType}.csv`, filetype: 'csv', thread_ts: msgWorkingOnIt.ts }) @@ -232,7 +236,7 @@ app.event('app_home_opened', async ({ payload, context, logger }) => { }) // Handle the shortcut for triggering manually scheduled jobs; -// this should only be used for debugging (so we dont have to wait until a triggered job would normally fire) +// this should only be used for debugging (so we don't have to wait until a triggered job would normally fire) app.shortcut('debug_manually_trigger_scheduled_jobs', async ({ ack, context, body }) => { // Acknowledge right away await ack() @@ -248,9 +252,9 @@ app.error(error => { (async () => { // Schedule our dynamic cron jobs - scheduleReminders() + scheduleJobs() - // Actually start thhe Bolt app. Let's go! + // Actually start the Bolt app. Let's go! await app.start(process.env.PORT || 3000) console.log('⚡️ Bolt app is running!') })() diff --git a/config.js b/config.js index 1b66775..702e493 100644 --- a/config.js +++ b/config.js @@ -21,9 +21,10 @@ const triageConfig = { emoji: ':white_circle:' } }, - scheduled_reminders: [ + scheduled_jobs: [ { expression: '0 * * * *', + type: 'triage', hours_to_look_back: 24, report_on_levels: ['Urgent', 'Medium'], // only report on messages with one of these levels ("OR" logic) report_on_does_not_have_status: ['Acknowledged', 'Done'] // only report on messages that do not have either of these statuses ("OR") diff --git a/docs/DEPLOY_Heroku.md b/docs/DEPLOY_Heroku.md index f3409b2..6cef23f 100644 --- a/docs/DEPLOY_Heroku.md +++ b/docs/DEPLOY_Heroku.md @@ -30,9 +30,9 @@ In a new tab, do the following. Be sure to replace `awesome-app-name-you-entered - Create a new Shortcut - Select **Global** shortcut - Choose a descriptive name and description for your shortcut, for example: - - Name: Show triage stats + - Name: Show channel stats - Description: Calculate stats for a triage channel - - For the Callback ID, it is important you set it to `triage_stats` + - For the Callback ID, it is important you set it to `channel_stats` - Enter your Select Menus Options Load URL `https://awesome-app-name-you-entered.herokuapp.com/slack/events` - Click Save Changes @@ -76,12 +76,12 @@ In a new tab, do the following. Be sure to replace `awesome-app-name-you-entered 2. Try out your freshly deployed app! 1. Visit your app's App Home tab to see the current configuration (you can edit `config.js` and restart the application to make changes) - 2. Execute your shortcut by entering "Show triage stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot. + 2. Execute your shortcut by entering "Show channel stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot. 3. Wait for the (by default) top-of-the-hour hourly update in any channel the bot has been invited to. 3. Take a moment to check out your Heroku addon. - You should see that MongoLabs has some data in it - Consider adding other addons to help you manage your app such as Logentries for ingesting your logs and NewRelic for monitoring performance characteristics. -4. Lastly, note that in the default configuration of this app, you should have one and only one web dyno running at a time as the scheduled reminder functionality runs _within_ the web application code courtesy of `node-cron`. +4. Lastly, note that in the default configuration of this app, you should have one and only one web dyno running at a time as the scheduled job functionality runs _within_ the web application code courtesy of `node-cron`. - In production, you may want to disable this and outsource the scheduling to [Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler) or another service/add-on. \ No newline at end of file diff --git a/docs/SETUP.md b/docs/SETUP.md index 1473ab8..fdd2121 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -25,9 +25,9 @@ In your preferred web browser: - Create a new Shortcut - Select **Global** shortcut - Choose a descriptive name and description for your shortcut, for example: - - Name: Show triage stats + - Name: Show channel stats - Description: Calculate stats for a triage channel - - For the Callback ID, it is important you set it to `triage_stats` + - For the Callback ID, it is important you set it to `channel_stats` - Enter your Select Menus Options Load URL `https://your-host/slack/events` @@ -94,8 +94,8 @@ Back in your preferred web browser... 2. Try out your app! 1. Visit your app's App Home tab to see the current configuration (you can edit `config.js` and restart the application to make changes) - 2. Execute your shortcut by entering "Show triage stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot. + 2. Execute your shortcut by entering "Show channel stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot. 3. Wait for the (by default) top-of-the-hour hourly update in any channel the bot has been invited to. -3. Lastly, note that in the default configuration of this app, you should have one and only one Node process running at a time as the scheduled reminder functionality runs _within_ the web application code courtesy of `node-cron`. +3. Lastly, note that in the default configuration of this app, you should have one and only one Node process running at a time as the scheduled job functionality runs _within_ the web application code courtesy of `node-cron`. - In production, you may want to disable this and outsource the scheduling to `crontab` or another schedule service/daemon. \ No newline at end of file diff --git a/docs/assets/func_1_scheduled_reminders.png b/docs/assets/func_1_scheduled_jobs.png similarity index 100% rename from docs/assets/func_1_scheduled_reminders.png rename to docs/assets/func_1_scheduled_jobs.png diff --git a/helpers/messages.js b/helpers/messages.js index 451c4fc..b2af09c 100644 --- a/helpers/messages.js +++ b/helpers/messages.js @@ -6,13 +6,13 @@ // We use json2csv to convert our messages JS object to a CSV file const { parse: parseJsonToCsv } = require('json2csv') -// Internal depencies +// Internal dependencies // Load our triage config const triageConfig = require('./../config') // The helper functions related to messages follow -// Recursive function to paginate through all history of a conversatoin +// Recursive function to paginate through all history of a conversation const getFullConvoHistory = async function (client, params, data = []) { const apiMethod = 'conversations.history' console.log(`Querying ${apiMethod} with ${JSON.stringify(params)}; already have ${data.length} in array`) @@ -51,7 +51,7 @@ const getAllMessagesForPastHours = async function (channelId, nHoursToGoBack, cl return allMessages } -const filterAndEnrichMessages = function (messages, fromChannel, teamBotId) { +const filterAndEnrichMessages = function (messages, fromChannel, teamBotId, statsType) { // First, filter out messages from the team's bot const filteredMessages = messages.filter(m => { if (m.bot_id !== teamBotId) return true @@ -62,38 +62,62 @@ const filterAndEnrichMessages = function (messages, fromChannel, teamBotId) { // Loop through all messages and enrich them additional attributes so we can do filters on them later enrichedMessages.forEach(message => { + // Regardless of statsType, we want to populate a few key/values // Add `channel` attribute with the channel we retrieved the message from message.channel = fromChannel - // Add array attributes we will populate later - message._statuses = [] - message._levels = [] - // Create a new `_reactions` attribute with an array of reacitons - message._reactions = message.reactions + // Create a new `_all_reactions` attribute with an array of all reactions (regardless of if they are relevant to triage analysis) + message._all_reactions = message.reactions ? message.reactions.map(r => `:${r.name}:`) : [] - // Populate `_level_XXX` attribute with boolean and add to array of _levels - triageConfig._.levels.forEach(level => { - if (message.text.includes(triageConfig._.levelToEmoji[level])) { - message[`_level_${level}`] = true - message._levels.push(level) - } - }) + // Add `_threadedReplyCount` and `_threadedReplyUsersCount` with the # of replies and # users who wrote those replies + message._threadedReplyCount = message.reply_count || 0 + message._threadedReplyUsersCount = message.reply_users_count || 0 - // Populate `_status_XXX` attribute with boolean and add to array of _statuses - triageConfig._.statuses.forEach(status => { - if (message._reactions.includes(triageConfig._.statusToEmoji[status])) { - message[`_status_${status}`] = true - message._statuses.push(status) + // If message is by a bot (such as workflow builder), put the bot ID in the user field + if (message.subtype === 'bot_message') { + message.user = message.bot_id + if (message.bot_profile.is_workflow_bot === true) { + message._postedByWorkflowBuilder = true } - }) + } + + if (statsType === 'triage') { + // Do additional status and level analysis for triage stats requests + // Add array attributes we will populate later + message._statuses = [] + message._levels = [] + + // Populate `_level_XXX` attribute with boolean and add to array of _levels + triageConfig._.levels.forEach(level => { + if (message.text.includes(triageConfig._.levelToEmoji[level])) { + message[`_level_${level}`] = true + message._levels.push(level) + } + }) + + // Populate `_status_XXX` attribute with boolean and add to array of _statuses + triageConfig._.statuses.forEach(status => { + if (message._all_reactions.includes(triageConfig._.statusToEmoji[status])) { + message[`_status_${status}`] = true + message._statuses.push(status) + } + }) + } else if (statsType === 'generic') { + // If generic analysis, let's dive deeper into the count of each emoji + // nothing currently added for generic analysis + message.reactions.forEach((reaction) => { + const key = `_nUsersReactedWith_${reaction.name}` + message[key] = reaction.count + }) + } }) return enrichedMessages } -const messagesToCsv = function (messages) { +const messagesToCsv = function (messages, statsType) { try { // Create CSV header row const csvFields = [ @@ -106,13 +130,35 @@ const messagesToCsv = function (messages) { 'text', 'blocks', 'attachments', - '_reactions', - '_levels', - '_statuses' + 'reactions', + '_postedByWorkflowBuilder', + '_all_reactions', + '_threadedReplyCount', + '_threadedReplyUsersCount' ] - const statusFields = triageConfig._.statuses.map(s => `_status_${s}`) - const levelFields = triageConfig._.levels.map(l => `_level_${l}`) - const csvOpts = { fields: csvFields.concat(levelFields, statusFields) } + + // add specific columns related to triage stats if relevant + if (statsType === 'triage') { + csvFields.push('_levels', '_stats') + + const statusFields = triageConfig._.statuses.map(s => `_status_${s}`) + const levelFields = triageConfig._.levels.map(l => `_level_${l}`) + + csvFields.push(...levelFields, ...statusFields) + } else if (statsType === 'generic') { + // Get a unique list of `nUsersReactedWith_` keys so we can use them as columns in the csv + const allReactionKeys = new Set() + messages.forEach((m) => { + Object.keys(m) + .filter((k) => k.includes('nUsersReactedWith_')) + .forEach((k) => allReactionKeys.add(k)) + }) + console.log(allReactionKeys) + csvFields.push(...Array.from(allReactionKeys)) + } + + console.log(csvFields) + const csvOpts = { fields: csvFields } const csvString = parseJsonToCsv(messages, csvOpts) return csvString } catch (e) { diff --git a/helpers/scheduled_jobs.js b/helpers/scheduled_jobs.js index 96681bc..606b3f0 100644 --- a/helpers/scheduled_jobs.js +++ b/helpers/scheduled_jobs.js @@ -14,10 +14,10 @@ const { getAllMessagesForPastHours, filterAndEnrichMessages } = require('./messa // Initialize a single instance of Slack Web API WebClient, without a token const client = new WebClient() -const onCronTick = async function (reminderConfig) { +const onCronTick = async function (jobConfig) { const now = new Date() console.log('[node-cron] [onCronTick] ran ' + now.toLocaleString()) - console.log('Reminder config is as follows', reminderConfig) + console.log('Job config is as follows', jobConfig) // Get all teams const teams = await AuthedTeam.find({}) @@ -48,25 +48,26 @@ const onCronTick = async function (reminderConfig) { // Get all messages from the beginning of time (probably not a good idea) const allMessages = await getAllMessagesForPastHours( channel.id, - reminderConfig.hours_to_look_back, + jobConfig.hours_to_look_back, client ) console.log( - `\tFound ${allMessages.length} total messages in past ${reminderConfig.hours_to_look_back} hours` + `\tFound ${allMessages.length} total messages in past ${jobConfig.hours_to_look_back} hours` ) - // Use the enricMessages helper to enrich the messages we have - const allMessagesEnriched = filterAndEnrichMessages(allMessages, channel, team.bot.id) + // Use the filterAndEnrichMessages helper to enrich the messages we have + // and always use 'triage' stats type for scheduled jobs + const allMessagesEnriched = filterAndEnrichMessages(allMessages, channel, team.bot.id, 'triage') // Filter all messages to ones we care about based off of the config const messagesFilteredForConfig = allMessagesEnriched.filter(m => { - // Look to see if the levels and statuses are any of the ones we care about (per reminderConfig) + // Look to see if the levels and statuses are any of the ones we care about (per jobConfig) const containsAnySpecifiedLevels = m._levels.some(l => - reminderConfig.report_on_levels.includes(l) + jobConfig.report_on_levels.includes(l) ) const containsAnySpecifiedStatuses = m._statuses.some(s => - reminderConfig.report_on_does_not_have_status.includes(s) + jobConfig.report_on_does_not_have_status.includes(s) ) // Return the boolean of if they contain any of the levels and do NOT contain any of the statuses @@ -74,21 +75,21 @@ const onCronTick = async function (reminderConfig) { }) console.log( - `\t${messagesFilteredForConfig.length} messages match the reminderConfig criteria` + `\t${messagesFilteredForConfig.length} messages match the jobConfig criteria` ) - const statusEmojis = reminderConfig.report_on_does_not_have_status.map( + const statusEmojis = jobConfig.report_on_does_not_have_status.map( s => triageConfig._.statusToEmoji[s] ) - const levelEmojis = reminderConfig.report_on_levels.map( + const levelEmojis = jobConfig.report_on_levels.map( l => triageConfig._.levelToEmoji[l] ) if (messagesFilteredForConfig.length === 0) { await client.chat.postMessage({ channel: channel.id, - text: `:tada: Nice job, <#${channel.id}>!` + - `There are ${messagesFilteredForConfig.length} messages from the past ${reminderConfig.hours_to_look_back} hours that are ` + + text: `:tada: Nice job, <#${channel.id}>! ` + + `There are ${messagesFilteredForConfig.length} messages from the past ${jobConfig.hours_to_look_back} hours that are ` + `either ${levelEmojis.join( '/' )} and don't have either ${statusEmojis.join('/')}` @@ -104,7 +105,7 @@ const onCronTick = async function (reminderConfig) { await client.chat.postMessage({ channel: channel.id, text: `:wave: Hi there, <#${channel.id}>. ` + - `${numMessagesString} from the past ${reminderConfig.hours_to_look_back} hours that are ` + + `${numMessagesString} from the past ${jobConfig.hours_to_look_back} hours that are ` + `either ${levelEmojis.join( '/' )} and don't have either ${statusEmojis.join( @@ -123,35 +124,35 @@ const onCronTick = async function (reminderConfig) { } // This exported function is loaded and executed toward the bottom of app.js -// when run, this function looks at all defined scheduled_reminders in the config js file +// when run, this function looks at all defined scheduled_jobs in the config js file // and schedules them! Upon triggering (handled by the node-cron) -const scheduleReminders = function () { - // get the scheduled_reminders array from the config file - const scheduledReminders = triageConfig.scheduled_reminders +const scheduleJobs = function () { + // get the scheduled_jobs array from the config file + const scheduledJobs = triageConfig.scheduled_jobs // check to make sure it is neither undefined nor blank - if (typeof (scheduledReminders) !== 'undefined' && scheduledReminders.length > 0) { - // For each reminder, schedule it based off of the expression value + if (typeof (scheduledJobs) !== 'undefined' && scheduledJobs.length > 0) { + // For each job, schedule it based off of the expression value // and send the rest of the config to the function (onCronTick) so it knows what to do - scheduledReminders.forEach(reminderConfig => { - cron.schedule(reminderConfig.expression, () => { - onCronTick(reminderConfig) + scheduledJobs.forEach(jobConfig => { + cron.schedule(jobConfig.expression, () => { + onCronTick(jobConfig) }) }) } else { - console.error('Sorry but there are no scheduled reminders to schedule.') - console.error('Please add some to config_triage.js and restart yoru app') + console.error('Sorry but there are no scheduled jobs to schedule.') + console.error('Please add some to config_triage.js and restart your app') } } const manuallyTriggerScheduledJobs = function () { console.debug('Manually triggering scheduled jobs') - triageConfig.scheduled_reminders.forEach(reminderConfig => { - onCronTick(reminderConfig) + triageConfig.scheduled_jobs.forEach(jobConfig => { + onCronTick(jobConfig) }) } module.exports = { - scheduleReminders, + scheduleJobs, onCronTick, manuallyTriggerScheduledJobs } diff --git a/views/app_home.blockkit.js b/views/app_home.blockkit.js index 8f6133b..c374e78 100644 --- a/views/app_home.blockkit.js +++ b/views/app_home.blockkit.js @@ -1,5 +1,6 @@ // External dependencies -// Crontstrue will help us convert cron expressions to human readable language +// Cronstrue will help us convert cron expressions to human readable language + const cronstrue = require('cronstrue') module.exports = function (userId, triageConfig) { @@ -22,19 +23,19 @@ module.exports = function (userId, triageConfig) { }) .join('\n') - const scheduledJobsDisplay = triageConfig.scheduled_reminders - .map(reminderConfig => { - const scheduleString = cronstrue.toString(reminderConfig.expression) - const levelEmojis = reminderConfig.report_on_levels.map( + const scheduledJobsDisplay = triageConfig.scheduled_jobs + .map(jobConfig => { + const scheduleString = cronstrue.toString(jobConfig.expression) + const levelEmojis = jobConfig.report_on_levels.map( l => triageConfig._.levelToEmoji[l] ) - const statusEmojis = reminderConfig.report_on_does_not_have_status.map( + const statusEmojis = jobConfig.report_on_does_not_have_status.map( s => triageConfig._.statusToEmoji[s] ) return `${indentationUsingWhitespaceHack.repeat( 2 )} ${scheduleString}, look for messages from the past ${ - reminderConfig.hours_to_look_back + jobConfig.hours_to_look_back } hours.. \n\t\t\tthat contain any of the following emoji: ${levelEmojis.join( ' / ' )}\n\t\t\tand do not have any of the following reactions: ${statusEmojis.join( @@ -63,7 +64,7 @@ module.exports = function (userId, triageConfig) { type: 'section', text: { type: 'mrkdwn', - text: `${newLineInSectionBlockHack}:question: *What is this?* :question:\n\nThis is the App Home for me, Triage Bot. I have two main jobs around here:\n\n:one: You can invite me to public channels and I will monitor for and remind the channel about messages that fit the criteria below (see _Scheduled Reminders_).\n\n:two: You can use my message shortcut :zap: to create ad-hoc reports on any public channel. I'll give you top-line stats and provide a CSV for offline analysis too.${newLineInSectionBlockHack.repeat( + text: `${newLineInSectionBlockHack}:question: *What is this?* :question:\n\nThis is the App Home for me, Triage Bot. I have two main jobs around here:\n\n:one: You can invite me to public channels and I will monitor for and remind the channel about messages that fit the criteria below (see _Scheduled Jobs_).\n\n:two: You can use my message shortcut :zap: to create ad-hoc reports on any public channel. I'll give you top-line stats and provide a CSV for offline analysis too.${newLineInSectionBlockHack.repeat( 2 )}` } @@ -107,7 +108,7 @@ module.exports = function (userId, triageConfig) { type: 'section', text: { type: 'mrkdwn', - text: `_Scheduled Reminders_\n\n${scheduledJobsDisplay}` + text: `_Scheduled Jobs_\n\n${scheduledJobsDisplay}` } }, { diff --git a/views/modals.blockkit.js b/views/modals.blockkit.js index cc6a7fa..d1be69b 100644 --- a/views/modals.blockkit.js +++ b/views/modals.blockkit.js @@ -1,10 +1,9 @@ module.exports = { - select_triage_channel: { + select_channel_and_config: { callback_id: 'channel_selected', - type: 'modal', title: { type: 'plain_text', - text: 'Triage stats', + text: 'Channel Stats', emoji: true }, submit: { @@ -12,6 +11,7 @@ module.exports = { text: 'Submit', emoji: true }, + type: 'modal', close: { type: 'plain_text', text: 'Cancel', @@ -22,8 +22,7 @@ module.exports = { type: 'section', text: { type: 'mrkdwn', - text: - ':wave: Please select a channel to retrieve triage stats for.\n\n:warning: Note that a bot :robot_face: will be added to the selected public channel.' + text: ':wave: Please select a channel to retrieve stats for.' } }, { @@ -32,6 +31,11 @@ module.exports = { { block_id: 'channel', type: 'input', + label: { + type: 'plain_text', + text: 'Select a channel', + emoji: true + }, element: { action_id: 'channel', type: 'conversations_select', @@ -42,18 +46,29 @@ module.exports = { }, default_to_current_conversation: true, filter: { - include: ['public'] + include: [ + 'public' + ] } - }, - label: { - type: 'plain_text', - text: 'Select a channel', - emoji: true } }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: 'A bot :robot_face: will be added to the channel' + } + ] + }, { block_id: 'n_hours', type: 'input', + label: { + type: 'plain_text', + text: 'How far back should we look?', + emoji: true + }, element: { action_id: 'n_hours', type: 'static_select', @@ -69,7 +84,6 @@ module.exports = { text: '7 days' } }, - options: [ { value: '12', @@ -107,11 +121,41 @@ module.exports = { } } ] - }, + } + }, + { + block_id: 'stats_type', + type: 'input', label: { type: 'plain_text', - text: ':1234: How far should we look back?', - emoji: true + text: 'What type of stats would you like to generate?' + }, + element: { + action_id: 'stats_type', + type: 'static_select', + initial_option: { + value: 'triage', + text: { + type: 'plain_text', + text: 'Triage (report on specific emojis)' + } + }, + options: [ + { + value: 'triage', + text: { + type: 'plain_text', + text: 'Triage (report on specific emojis)' + } + }, + { + value: 'generic', + text: { + type: 'plain_text', + text: 'Generic (report on all emoji reactions)' + } + } + ] } } ]