import { getInteractionIdRegexpFromCommandName, GrowiCommand, GrowiCommandProcessor, GrowiInteractionProcessor, InteractionHandledResult, markdownSectionBlock, replaceOriginal, respond, InteractionPayloadAccessor, } from '@growi/slack'; import { AuthorizeResult } from '@slack/oauth'; import { Inject, Service } from '@tsed/di'; import { Installation } from '~/entities/installation'; import { Relation } from '~/entities/relation'; import { InstallationRepository } from '~/repositories/installation'; import { RelationRepository } from '~/repositories/relation'; import loggerFactory from '~/utils/logger'; const logger = loggerFactory('slackbot-proxy:services:UnregisterService'); export type SelectGrowiCommandBody = { growiUrisForSingleUse: string[], } type SelectValue = { growiCommand: GrowiCommand, growiUri: any, } type SendCommandBody = { // eslint-disable-next-line camelcase trigger_id: string, // eslint-disable-next-line camelcase channel_id: string, // eslint-disable-next-line camelcase channel_name: string, } export type SelectedGrowiInformation = { relation: Relation, growiCommand: GrowiCommand, sendCommandBody: SendCommandBody, } @Service() export class SelectGrowiService implements GrowiCommandProcessor, GrowiInteractionProcessor { @Inject() relationRepository: RelationRepository; @Inject() installationRepository: InstallationRepository; private generateGrowiSelectValue(growiCommand: GrowiCommand, growiUri: string): SelectValue { return { growiCommand, growiUri, }; } shouldHandleCommand(): boolean { // TODO: consider to use the default supported commands for single use return true; } async processCommand( growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, context: SelectGrowiCommandBody, ): Promise { const growiUrls = context.growiUrisForSingleUse; const chooseSection = growiUrls.map((growiUri) => { const value = this.generateGrowiSelectValue(growiCommand, growiUri); return ({ type: 'section', text: { type: 'mrkdwn', text: growiUri, }, accessory: { type: 'button', action_id: 'select_growi:select_growi', text: { type: 'plain_text', text: 'Choose', }, value: JSON.stringify(value), }, }); }); return respond(growiCommand.responseUrl, { blocks: [ { type: 'header', text: { type: 'plain_text', text: 'Select target GROWI', }, }, { type: 'context', elements: [ { type: 'mrkdwn', text: `Request: \`/growi ${growiCommand.text}\` to:`, }, ], }, { type: 'divider', }, ...chooseSection, ], }); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types shouldHandleInteraction(interactionPayloadAccessor: InteractionPayloadAccessor): boolean { const { actionId, callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad(); const registerRegexp: RegExp = getInteractionIdRegexpFromCommandName('select_growi'); return registerRegexp.test(actionId) || registerRegexp.test(callbackId); } async processInteraction( // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor, ): Promise> { const interactionHandledResult: InteractionHandledResult = { isTerminated: false, }; if (!this.shouldHandleInteraction(interactionPayloadAccessor)) return interactionHandledResult; const selectGrowiInformation = await this.handleSelectInteraction(authorizeResult, interactionPayload, interactionPayloadAccessor); if (selectGrowiInformation != null) { interactionHandledResult.result = selectGrowiInformation; } interactionHandledResult.isTerminated = false; return interactionHandledResult as InteractionHandledResult; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async handleSelectInteraction( // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor, ): Promise { const responseUrl = interactionPayloadAccessor.getResponseUrl(); const selectGrowiValue = interactionPayloadAccessor.firstAction()?.value; if (selectGrowiValue == null) { logger.error('Growi command failed: The first action element must have the value parameter.'); await respond(responseUrl, { text: 'Growi command failed', blocks: [ markdownSectionBlock('Error occurred while processing GROWI command.'), ], }); return null; } const { growiUri, growiCommand } = JSON.parse(selectGrowiValue); if (growiCommand == null) { logger.error('Growi command failed: The first action value must have growiCommand parameter.'); await respond(responseUrl, { text: 'Growi command failed', blocks: [ markdownSectionBlock('Error occurred while processing GROWI command.'), ], }); return null; } await replaceOriginal(responseUrl, { text: `Accepted ${growiCommand.growiCommandType} command.`, blocks: [ markdownSectionBlock(`Forwarding your request *"/growi ${growiCommand.growiCommandType}"* on GROWI to ${growiUri} ...`), ], }); const installationId = authorizeResult.enterpriseId || authorizeResult.teamId; let installation: Installation | undefined; try { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!); } catch (err) { logger.error('Growi command failed: No installation found.\n', err); await respond(responseUrl, { text: 'Growi command failed', blocks: [ markdownSectionBlock('Error occurred while processing GROWI command.'), ], }); return null; } const relation = await this.relationRepository.createQueryBuilder('relation') .where('relation.growiUri =:growiUri', { growiUri }) .andWhere('relation.installationId = :id', { id: installation?.id }) .leftJoinAndSelect('relation.installation', 'installation') .getOne(); if (relation == null) { logger.error('Growi command failed: No installation found.'); await respond(responseUrl, { text: 'Growi command failed', blocks: [ markdownSectionBlock('Error occurred while processing GROWI command.'), ], }); return null; } // increment sendCommandBody const channel = interactionPayloadAccessor.getChannel(); if (channel == null) { logger.error('Growi command failed: channel not found.'); await respond(responseUrl, { text: 'Growi command failed', blocks: [ markdownSectionBlock('Error occurred while processing GROWI command.'), ], }); return null; } const sendCommandBody: SendCommandBody = { trigger_id: interactionPayload.trigger_id, channel_id: channel.id, channel_name: channel.name, }; return { relation, growiCommand, sendCommandBody, }; } }