Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bots/bugbuster/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CommandProcessor } from './services/command-processor'
import { IssueProcessor } from './services/issue-processor'
import { IssueStateChecker } from './services/issue-state-checker'
import { RecentlyLintedManager } from './services/recently-linted-manager'
Expand All @@ -14,6 +15,7 @@ export const bootstrap = (props: types.CommonHandlerProps) => {
const recentlyLintedManager = new RecentlyLintedManager(linear)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager)
const issueStateChecker = new IssueStateChecker(linear, logger)
const commandProcessor = new CommandProcessor(client, teamsManager, ctx.botId)

return {
botpress,
Expand All @@ -22,5 +24,6 @@ export const bootstrap = (props: types.CommonHandlerProps) => {
recentlyLintedManager,
issueProcessor,
issueStateChecker,
commandProcessor,
}
}
133 changes: 38 additions & 95 deletions bots/bugbuster/src/handlers/message-created.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { CommandDefinition } from 'src/types'
import * as boot from '../bootstrap'
import * as bp from '.botpress'

const MESSAGING_INTEGRATIONS = ['telegram', 'slack']
const COMMAND_LIST_MESSAGE = `Unknown command. Here's a list of possible commands:
#health
#addTeam [teamName]
#removeTeam [teamName]
#listTeams
#lintAll
#getNotifChannel
#setNotifChannel [channelName]`
const ARGUMENT_REQUIRED_MESSAGE = 'Error: an argument is required with this command.'

export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => {
const { conversation, message, client, ctx } = props
const { conversation, message } = props
if (!MESSAGING_INTEGRATIONS.includes(conversation.integration)) {
props.logger.info(`Ignoring message from ${conversation.integration}`)
return
Expand All @@ -25,107 +17,58 @@ export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => {
return
}

const { botpress, teamsManager } = boot.bootstrap(props)
const { botpress, commandProcessor } = boot.bootstrap(props)
const commandListMessage = _buildListCommandsMessage(commandProcessor.commandDefinitions)

if (message.type !== 'text') {
await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE)
await botpress.respondText(conversation.id, commandListMessage)
return
}

if (!message.payload.text) {
await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE)
await botpress.respondText(conversation.id, commandListMessage)
return
}

const [command, arg1] = message.payload.text.trim().split(' ')
const [command, ...args] = message.payload.text.trim().split(' ')
if (!command) {
await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE)
await botpress.respondText(conversation.id, commandListMessage)
return
}

const _handleError = (context: string) => (thrown: unknown) =>
botpress.handleError({ context, conversationId: conversation.id }, thrown)
const commandDefinition = commandProcessor.commandDefinitions.find((commandImpl) => commandImpl.name === command)
if (!commandDefinition) {
await botpress.respondText(conversation.id, commandListMessage)
return
}

switch (command) {
case '#addTeam': {
if (!arg1) {
await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE)
return
}
if (commandDefinition.requiredArgs && args.length < commandDefinition.requiredArgs?.length) {
await botpress.respondText(
conversation.id,
`Error: a minimum of ${commandDefinition.requiredArgs.length} argument(s) is required.`
)
return
}

await teamsManager.addWatchedTeam(arg1).catch(_handleError('trying to add a team'))
const _handleError = (context: string, thrown: unknown) =>
botpress.handleError({ context, conversationId: conversation.id }, thrown)

await botpress.respondText(
conversation.id,
`Success: the team with the key '${arg1}' has been added to the watched team list.`
)
break
}
case '#removeTeam': {
if (!arg1) {
await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE)
return
}
try {
const result = await commandDefinition.implementation(args, conversation.id)
await botpress.respondText(conversation.id, `${result.success ? '' : 'Error: '}${result.message}`)
} catch (thrown) {
await _handleError(`trying to run ${commandDefinition.name}`, thrown)
}
}

await teamsManager.removeWatchedTeam(arg1).catch(_handleError('trying to remove a team'))
await botpress.respondText(
conversation.id,
`Success: the team with the key '${arg1}' has been removed from the watched team list.`
)
break
}
case '#listTeams': {
const teams = await teamsManager.listWatchedTeams().catch(_handleError('trying to list teams'))
await botpress.respondText(conversation.id, teams.join(', '))
break
}
case '#lintAll': {
await client.getOrCreateWorkflow({
name: 'lintAll',
input: {},
discriminateByStatusGroup: 'active',
conversationId: conversation.id,
status: 'pending',
})
const _buildListCommandsMessage = (definitions: CommandDefinition[]) => {
const commands = definitions.map(_buildCommandMessage).join('\n')
return `Unknown command. Here's a list of possible commands:\n${commands}`
}

await botpress.respondText(conversation.id, "Launched 'lintAll' workflow.")
break
}
case '#setNotifChannel': {
if (!arg1) {
await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE)
return
}
await client.setState({
id: ctx.botId,
name: 'notificationChannelName',
type: 'bot',
payload: { name: arg1 },
})
await botpress.respondText(conversation.id, `Success. Notification channel is now set to ${arg1}.`)
break
}
case '#getNotifChannel': {
const {
state: {
payload: { name },
},
} = await client.getOrSetState({
id: ctx.botId,
name: 'notificationChannelName',
type: 'bot',
payload: {},
})
let message = 'There is no set Slack notification channel.'
if (name) {
message = `The Slack notification channel is ${name}.`
}
await botpress.respondText(conversation.id, message)
break
}
default: {
await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE)
break
}
}
const _buildCommandMessage = (definition: CommandDefinition) => {
const requiredArgs = definition.requiredArgs?.map((arg) => `<${arg}>`).join(' ')
const optionalArgs = definition.optionalArgs?.map((arg) => `[${arg}]`).join(' ')

return `${definition.name} ${requiredArgs ?? ''} ${optionalArgs ?? ''}`
}
129 changes: 129 additions & 0 deletions bots/bugbuster/src/services/command-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as types from '../types'
import { TeamsManager } from './teams-manager'
import { Client } from '.botpress'

const MISSING_ARGS_ERROR = 'More arguments are required with this command.'

export class CommandProcessor {
public constructor(
private _client: Client,
private _teamsManager: TeamsManager,
private _botId: string
) {}

private _listTeams: types.CommandImplementation = async () => {
const teams = await this._teamsManager.listWatchedTeams()
return { success: true, message: teams.join(', ') }
}

private _addTeam: types.CommandImplementation = async ([team]: string[]) => {
if (!team) {
return { success: false, message: MISSING_ARGS_ERROR }
}

await this._teamsManager.addWatchedTeam(team)

return {
success: true,
message: `Success: the team with the key '${team}' has been added to the watched team list.`,
}
}

private _removeTeam: types.CommandImplementation = async ([team]: string[]) => {
if (!team) {
return { success: false, message: MISSING_ARGS_ERROR }
}

await this._teamsManager.removeWatchedTeam(team)

return {
success: true,
message: `Success: the team with the key '${team}' has been removed from the watched team list.`,
}
}

private _lintAll: types.CommandImplementation = async (_: string[], conversationId: string) => {
await this._client.getOrCreateWorkflow({
name: 'lintAll',
input: {},
discriminateByStatusGroup: 'active',
conversationId,
status: 'pending',
})

return {
success: true,
message: "Launched 'lintAll' workflow.",
}
}

private _setNotifChannel: types.CommandImplementation = async ([channel]: string[]) => {
if (!channel) {
return { success: false, message: MISSING_ARGS_ERROR }
}
await this._client.setState({
id: this._botId,
name: 'notificationChannelName',
type: 'bot',
payload: { name: channel },
})

return {
success: true,
message: `Success. Notification channel is now set to ${channel}.`,
}
}

private _getNotifChannel: types.CommandImplementation = async () => {
const {
state: {
payload: { name },
},
} = await this._client.getOrSetState({
id: this._botId,
name: 'notificationChannelName',
type: 'bot',
payload: {},
})

let message = 'There is no set Slack notification channel.'
if (name) {
message = `The Slack notification channel is ${name}.`
}

return {
success: true,
message,
}
}

public commandDefinitions: types.CommandDefinition[] = [
{
name: '#listTeams',
implementation: this._listTeams,
},
{
name: '#addTeam',
implementation: this._addTeam,
requiredArgs: ['teamName'],
},
{
name: '#removeTeam',
implementation: this._removeTeam,
requiredArgs: ['teamName'],
},
{
name: '#lintAll',
implementation: this._lintAll,
},
{
name: '#setNotifChannel',
implementation: this._setNotifChannel,
requiredArgs: ['channelName'],
},
{
name: '#getNotifChannel',
implementation: this._getNotifChannel,
},
]
}
9 changes: 9 additions & 0 deletions bots/bugbuster/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ export type StateAttributes = {
}

export type ISO8601Duration = string

type CommandResult = { success: boolean; message: string }
export type CommandImplementation = (args: string[], conversationId: string) => CommandResult | Promise<CommandResult>
export type CommandDefinition = {
name: string
requiredArgs?: string[]
optionalArgs?: string[]
implementation: CommandImplementation
}
Loading
Loading