diff --git a/bots/bugbuster/bot.definition.ts b/bots/bugbuster/bot.definition.ts index 364f7e99e49..8ab3c8dfe32 100644 --- a/bots/bugbuster/bot.definition.ts +++ b/bots/bugbuster/bot.definition.ts @@ -40,14 +40,21 @@ export default new sdk.BotDefinition({ ), }), }, - notificationChannelName: { + notificationChannels: { type: 'bot', schema: sdk.z.object({ - name: sdk.z - .string() - .optional() - .title('Notification Channel Name') - .describe('The Slack channel where notifications will be posted'), + channels: sdk.z + .array( + sdk.z.object({ + name: sdk.z.string().title('Name').describe('The channel name'), + teams: sdk.z + .array(sdk.z.string()) + .title('Teams') + .describe('The teams for which notifications will be sent to the channel'), + }) + ) + .title('Channel') + .describe('The Slack channel where notifications will be sent'), }), }, }, diff --git a/bots/bugbuster/src/handlers/lint-all.ts b/bots/bugbuster/src/handlers/lint-all.ts index 193ff0e9c0f..02430308283 100644 --- a/bots/bugbuster/src/handlers/lint-all.ts +++ b/bots/bugbuster/src/handlers/lint-all.ts @@ -3,14 +3,11 @@ import * as boot from '../bootstrap' import * as bp from '.botpress' export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => { - const { client, workflow, conversation } = props - - const conversationId = conversation?.id + const { client, workflow, ctx, conversation } = props const { botpress, issueProcessor } = boot.bootstrap(props) - const _handleError = (context: string) => (thrown: unknown) => - botpress.handleError({ context, conversationId }, thrown) + const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown) const { state: { @@ -75,8 +72,33 @@ export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => { endCursor = pagedIssues.pagination?.endCursor } while (hasNextPage) - if (conversationId) { - await botpress.respondText(conversationId, _buildResultMessage(lintResults)).catch(() => {}) + if (conversation?.id) { + await botpress.respondText(conversation.id, _buildResultMessage(lintResults)).catch(() => {}) + await workflow.setCompleted() + return + } + + const { + state: { + payload: { channels }, + }, + } = await client.getOrSetState({ + id: ctx.botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels: [] }, + }) + + for (const channel of channels) { + const conversationId = await _getConversationId(client, channel.name).catch( + _handleError(`trying to get the conversation ID of Slack channel '${channel.name}'`) + ) + + const relevantIssues = lintResults.filter((result) => + channel.teams.some((team) => result.identifier.includes(team)) + ) + + await botpress.respondText(conversationId, _buildResultMessage(relevantIssues)).catch(() => {}) } await workflow.setCompleted() @@ -103,3 +125,13 @@ const _buildResultMessage = (results: types.LintResult[]) => { return `Linting complete. ${messageDetail}` } + +const _getConversationId = async (client: bp.Client, channelName: string) => { + const conversation = await client.callAction({ + type: 'slack:startChannelConversation', + input: { + channelName, + }, + }) + return conversation.output.conversationId +} diff --git a/bots/bugbuster/src/handlers/time-to-lint-all.ts b/bots/bugbuster/src/handlers/time-to-lint-all.ts index ed707ce51ed..7fa8caf13ba 100644 --- a/bots/bugbuster/src/handlers/time-to-lint-all.ts +++ b/bots/bugbuster/src/handlers/time-to-lint-all.ts @@ -2,49 +2,19 @@ import * as boot from '../bootstrap' import * as bp from '.botpress' export const handleTimeToLintAll: bp.EventHandlers['timeToLintAll'] = async (props) => { - const { client, ctx, logger } = props + const { client, logger } = props logger.info("'timeToLintAll' event received.") const { botpress } = boot.bootstrap(props) - const _handleError = (context: string, conversationId?: string) => (thrown: unknown) => - botpress.handleError({ context, conversationId }, thrown) - - const conversationId = await _tryGetConversationId(client, ctx.botId).catch( - _handleError('trying to get Slack notification channel') - ) + const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown) await client .getOrCreateWorkflow({ name: 'lintAll', input: {}, discriminateByStatusGroup: 'active', - conversationId, status: 'pending', }) - .catch(_handleError("trying to start the 'lintAll' workflow", conversationId)) -} - -const _tryGetConversationId = async (client: bp.Client, botId: string): Promise => { - const { - state: { - payload: { name }, - }, - } = await client.getOrSetState({ - id: botId, - name: 'notificationChannelName', - payload: {}, - type: 'bot', - }) - - if (name) { - const conversation = await client.callAction({ - type: 'slack:startChannelConversation', - input: { - channelName: name, - }, - }) - return conversation.output.conversationId - } - return undefined + .catch(_handleError("trying to start the 'lintAll' workflow")) } diff --git a/bots/bugbuster/src/services/command-processor.ts b/bots/bugbuster/src/services/command-processor.ts index 5b01565fc74..e7610d68205 100644 --- a/bots/bugbuster/src/services/command-processor.ts +++ b/bots/bugbuster/src/services/command-processor.ts @@ -57,38 +57,151 @@ export class CommandProcessor { } } - private _setNotifChannel: types.CommandImplementation = async ([channel]: string[]) => { - if (!channel) { + private _addNotifChannel: types.CommandImplementation = async ([channelToAdd, ...teams]: string[]) => { + if (!channelToAdd || !teams[0]) { + return { success: false, message: MISSING_ARGS_ERROR } + } + + const { + state: { + payload: { channels }, + }, + } = await this._client.getOrSetState({ + id: this._botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels: [] }, + }) + + const watchedTeams = await this._teamsManager.listWatchedTeams() + if (!teams.every((team) => watchedTeams.includes(team))) { + return { + success: false, + message: 'make sure every team you want to add is being watched.', + } + } + + const existingChannel = channels.find((channel) => channel.name === channelToAdd) + if (!existingChannel) { + channels.push({ name: channelToAdd, teams }) + } else { + teams.forEach((team) => { + if (!existingChannel.teams.includes(team)) { + existingChannel.teams.push(team) + } + }) + } + + await this._client.setState({ + id: this._botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels }, + }) + + return { + success: true, + message: `Notifications for team(s) ${teams.join(', ')} will be posted in channel ${channelToAdd}.`, + } + } + + private _removeNotifChannel: types.CommandImplementation = async ([channelToRemove]: string[]) => { + if (!channelToRemove) { + return { success: false, message: MISSING_ARGS_ERROR } + } + + const { + state: { + payload: { channels }, + }, + } = await this._client.getOrSetState({ + id: this._botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels: [] }, + }) + + if (!channels.find((channel) => channel.name === channelToRemove)) { + return { + success: false, + message: `channel '${channelToRemove}' is not part of the notification channels.`, + } + } + + await this._client.setState({ + id: this._botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels: channels.filter((channel) => channel.name !== channelToRemove) }, + }) + + return { + success: true, + message: `Notification channel ${channelToRemove} has been removed.`, + } + } + + private _removeNotifChannelTeam: types.CommandImplementation = async ([channelToRemove, teamToRemove]: string[]) => { + if (!channelToRemove || !teamToRemove) { return { success: false, message: MISSING_ARGS_ERROR } } + + const { + state: { + payload: { channels }, + }, + } = await this._client.getOrSetState({ + id: this._botId, + name: 'notificationChannels', + type: 'bot', + payload: { channels: [] }, + }) + + const channel = channels.find((channel) => channel.name === channelToRemove) + if (!channel) { + return { + success: false, + message: `channel '${channelToRemove}' is not part of the notification channels.`, + } + } + + if (!channel.teams.find((team) => team === teamToRemove)) { + return { + success: false, + message: `channel ${channel.name} does not receive notifications from team '${teamToRemove}'.`, + } + } + + channel.teams = channel.teams.filter((team) => team !== teamToRemove) + await this._client.setState({ id: this._botId, - name: 'notificationChannelName', + name: 'notificationChannels', type: 'bot', - payload: { name: channel }, + payload: { channels }, }) return { success: true, - message: `Success. Notification channel is now set to ${channel}.`, + message: `Team ${teamToRemove} has been removed from channel ${channelToRemove}.`, } } - private _getNotifChannel: types.CommandImplementation = async () => { + private _listNotifChannels: types.CommandImplementation = async () => { const { state: { - payload: { name }, + payload: { channels }, }, } = await this._client.getOrSetState({ id: this._botId, - name: 'notificationChannelName', + name: 'notificationChannels', type: 'bot', - payload: {}, + payload: { channels: [] }, }) let message = 'There is no set Slack notification channel.' - if (name) { - message = `The Slack notification channel is ${name}.` + if (channels.length > 0) { + message = this._buildNotifChannelsMessage(channels) } return { @@ -97,6 +210,15 @@ export class CommandProcessor { } } + private _buildNotifChannelsMessage(channels: { name: string; teams: string[] }[]) { + return `The Slack notification channels are:\n${channels.map(this._getMessageForChannel).join('\n')}` + } + + private _getMessageForChannel(channel: { name: string; teams: string[] }) { + const { name, teams } = channel + return `- channel ${name} for team(s) ${teams.join(', ')}` + } + public commandDefinitions: types.CommandDefinition[] = [ { name: '#listTeams', @@ -117,13 +239,24 @@ export class CommandProcessor { implementation: this._lintAll, }, { - name: '#setNotifChannel', - implementation: this._setNotifChannel, + name: '#addNotifChannel', + implementation: this._addNotifChannel, + requiredArgs: ['channelName', 'teamName1'], + optionalArgs: ['teamName2 ...'], + }, + { + name: '#removeNotifChannel', + implementation: this._removeNotifChannel, requiredArgs: ['channelName'], }, { - name: '#getNotifChannel', - implementation: this._getNotifChannel, + name: '#removeNotifChannelTeam', + implementation: this._removeNotifChannelTeam, + requiredArgs: ['channelName', 'teamName'], + }, + { + name: '#listNotifChannels', + implementation: this._listNotifChannels, }, ] } diff --git a/bots/bugbuster/src/services/teams-manager.ts b/bots/bugbuster/src/services/teams-manager.ts index 1904ee06de7..d24a9cb14c3 100644 --- a/bots/bugbuster/src/services/teams-manager.ts +++ b/bots/bugbuster/src/services/teams-manager.ts @@ -25,6 +25,7 @@ export class TeamsManager { if (!teamKeys.includes(key)) { throw new Error(`The team with the key '${key}' is not currently being watched.`) } + await this._setWatchedTeams(teamKeys.filter((team) => team !== key)) } public async listWatchedTeams(): Promise {