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.
- 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
- Triage stats bot will be added to the specified channel and run its analysis
- 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)'
+ }
+ }
+ ]
}
}
]