| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { toNonBlankStringOrUndefined } from '@growi/core/dist/interfaces';
- import {
- type GrowiBotEvent,
- type GrowiCommand,
- SlackbotType,
- } from '@growi/slack';
- import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
- import type { InteractionPayloadAccessor } from '@growi/slack/dist/utils/interaction-payload-accessor';
- import type { RespondUtil } from '@growi/slack/dist/utils/respond-util-factory';
- import { generateWebClient } from '@growi/slack/dist/utils/webclient-factory';
- import type { ChatPostMessageArguments, WebClient } from '@slack/web-api';
- import type { IncomingWebhookSendArguments } from '@slack/webhook';
- import mongoose from 'mongoose';
- import loggerFactory from '~/utils/logger';
- import type Crowi from '../crowi';
- import type { EventActionsPermission } from '../interfaces/slack-integration/events';
- import S2sMessage from '../models/vo/s2s-message';
- import { SlackCommandHandlerError } from '../models/vo/slack-command-handler-error';
- import { slackLegacyUtilFactory } from '../util/slack-legacy';
- import { configManager } from './config-manager';
- import type { S2sMessagingService } from './s2s-messaging/base';
- import type { S2sMessageHandlable } from './s2s-messaging/handlable';
- import { LinkSharedEventHandler } from './slack-event-handler/link-shared';
- const logger = loggerFactory('growi:service:SlackBotService');
- const OFFICIAL_SLACKBOT_PROXY_URI = 'https://slackbot-proxy.growi.org';
- type S2sMessageForSlackIntegration = S2sMessage & { updatedAt: Date };
- export class SlackIntegrationService implements S2sMessageHandlable {
- crowi: Crowi;
- s2sMessagingService!: S2sMessagingService;
- lastLoadedAt?: Date;
- linkSharedHandler!: LinkSharedEventHandler;
- constructor(crowi: Crowi) {
- this.crowi = crowi;
- this.s2sMessagingService = crowi.s2sMessagingService;
- this.linkSharedHandler = new LinkSharedEventHandler(crowi);
- this.initialize();
- }
- initialize() {
- this.lastLoadedAt = new Date();
- }
- /**
- * @inheritdoc
- */
- shouldHandleS2sMessage(s2sMessage: S2sMessageForSlackIntegration): boolean {
- const { eventName, updatedAt } = s2sMessage;
- if (eventName !== 'slackIntegrationServiceUpdated' || updatedAt == null) {
- return false;
- }
- return (
- this.lastLoadedAt == null ||
- this.lastLoadedAt < new Date(s2sMessage.updatedAt)
- );
- }
- /**
- * @inheritdoc
- */
- async handleS2sMessage(): Promise<void> {
- const { configManager } = this.crowi;
- logger.info('Reset slack bot by pubsub notification');
- await configManager.loadConfigs();
- this.initialize();
- }
- async publishUpdatedMessage(): Promise<void> {
- const { s2sMessagingService } = this;
- if (s2sMessagingService != null) {
- const s2sMessage = new S2sMessage('slackIntegrationServiceUpdated', {
- updatedAt: new Date(),
- });
- try {
- await s2sMessagingService.publish(s2sMessage);
- } catch (e) {
- logger.error(
- 'Failed to publish update message with S2sMessagingService: ',
- e.message,
- );
- }
- }
- }
- get isSlackConfigured(): boolean {
- return this.isSlackbotConfigured || this.isSlackLegacyConfigured;
- }
- get isSlackbotConfigured(): boolean {
- const hasSlackbotType = !!configManager.getConfig(
- 'slackbot:currentBotType',
- );
- return hasSlackbotType;
- }
- get isSlackLegacyConfigured(): boolean {
- // for legacy util
- const hasSlackToken = !!configManager.getConfig('slack:token');
- const hasSlackIwhUrl = !!configManager.getConfig(
- 'slack:incomingWebhookUrl',
- );
- return hasSlackToken || hasSlackIwhUrl;
- }
- private isCheckTypeValid(): boolean {
- const currentBotType = configManager.getConfig('slackbot:currentBotType');
- if (currentBotType == null) {
- throw new Error(
- "The config 'SLACKBOT_TYPE'(ns: 'crowi', key: 'slackbot:currentBotType') must be set.",
- );
- }
- return true;
- }
- get proxyUriForCurrentType(): string | undefined {
- const currentBotType = configManager.getConfig('slackbot:currentBotType');
- // TODO assert currentBotType is not null and CUSTOM_WITHOUT_PROXY
- switch (currentBotType) {
- case SlackbotType.OFFICIAL:
- return OFFICIAL_SLACKBOT_PROXY_URI;
- default:
- return toNonBlankStringOrUndefined(
- configManager.getConfig('slackbot:proxyUri'),
- );
- }
- }
- /**
- * generate WebClient instance for CUSTOM_WITHOUT_PROXY type
- */
- async generateClientForCustomBotWithoutProxy(): Promise<WebClient> {
- this.isCheckTypeValid();
- const token = configManager.getConfig('slackbot:withoutProxy:botToken');
- if (token == null) {
- throw new Error(
- "The config 'SLACK_BOT_TOKEN'(ns: 'crowi', key: 'slackbot:withoutProxy:botToken') must be set.",
- );
- }
- return generateWebClient(token);
- }
- /**
- * generate WebClient instance by tokenPtoG
- * @param tokenPtoG
- */
- async generateClientByTokenPtoG(tokenPtoG: string): Promise<WebClient> {
- this.isCheckTypeValid();
- const SlackAppIntegration = mongoose.model('SlackAppIntegration');
- const slackAppIntegration = await SlackAppIntegration.findOne({
- tokenPtoG,
- });
- if (slackAppIntegration == null) {
- throw new Error(
- 'No SlackAppIntegration exists that corresponds to the tokenPtoG specified.',
- );
- }
- return this.generateClientBySlackAppIntegration(
- slackAppIntegration as unknown as { tokenGtoP: string },
- );
- }
- /**
- * generate WebClient instance by tokenPtoG
- * @param tokenPtoG
- */
- async generateClientForPrimaryWorkspace(): Promise<WebClient> {
- this.isCheckTypeValid();
- const currentBotType = configManager.getConfig('slackbot:currentBotType');
- if (currentBotType === SlackbotType.CUSTOM_WITHOUT_PROXY) {
- return this.generateClientForCustomBotWithoutProxy();
- }
- // retrieve primary SlackAppIntegration
- const SlackAppIntegration = mongoose.model('SlackAppIntegration');
- const slackAppIntegration = await SlackAppIntegration.findOne({
- isPrimary: true,
- });
- if (slackAppIntegration == null) {
- throw new Error('None of the primary SlackAppIntegration exists.');
- }
- return this.generateClientBySlackAppIntegration(
- slackAppIntegration as unknown as { tokenGtoP: string },
- );
- }
- /**
- * generate WebClient instance by SlackAppIntegration
- * @param slackAppIntegration
- */
- async generateClientBySlackAppIntegration(slackAppIntegration: {
- tokenGtoP: string;
- }): Promise<WebClient> {
- this.isCheckTypeValid();
- // connect to proxy
- const serverUri = new URL('/g2s', this.proxyUriForCurrentType);
- const headers = {
- 'x-growi-gtop-tokens': slackAppIntegration.tokenGtoP,
- };
- return generateWebClient(undefined, serverUri.toString(), headers);
- }
- async postMessage(
- messageArgs: ChatPostMessageArguments,
- slackAppIntegration?: { tokenGtoP: string },
- ): Promise<void> {
- // use legacy slack configuration
- if (this.isSlackLegacyConfigured && !this.isSlackbotConfigured) {
- return this.postMessageWithLegacyUtil(messageArgs);
- }
- const client =
- slackAppIntegration == null
- ? await this.generateClientForPrimaryWorkspace()
- : await this.generateClientBySlackAppIntegration(slackAppIntegration);
- try {
- await client.chat.postMessage(messageArgs);
- } catch (error) {
- logger.debug({ err: error }, 'Post error');
- logger.debug({ messageArgs }, 'Sent data to slack');
- throw error;
- }
- }
- private async postMessageWithLegacyUtil(
- messageArgs: ChatPostMessageArguments | IncomingWebhookSendArguments,
- ): Promise<void> {
- const slackLegacyUtil = slackLegacyUtilFactory(configManager);
- try {
- await slackLegacyUtil.postMessage(messageArgs);
- } catch (error) {
- logger.debug({ err: error }, 'Post error');
- logger.debug({ messageArgs }, 'Sent data to slack');
- throw error;
- }
- }
- /**
- * Handle /commands endpoint
- */
- async handleCommandRequest(
- growiCommand: GrowiCommand,
- client,
- body,
- respondUtil: RespondUtil,
- ): Promise<void> {
- const { growiCommandType } = growiCommand;
- const modulePath = `./slack-command-handler/${growiCommandType}`;
- let handler: any;
- try {
- handler = require(modulePath)(this.crowi);
- } catch (err) {
- const text = `*No command.*\n \`command: ${growiCommand.text}\``;
- logger.error(err);
- throw new SlackCommandHandlerError(text, {
- respondBody: {
- text,
- blocks: [
- markdownSectionBlock(
- '*No command.*\n Hint\n `/growi [command] [keyword]`',
- ),
- ],
- },
- });
- }
- // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
- return handler.handleCommand(growiCommand, client, body, respondUtil);
- }
- async handleBlockActionsRequest(
- client,
- interactionPayload: any,
- interactionPayloadAccessor: InteractionPayloadAccessor,
- respondUtil: RespondUtil,
- ): Promise<void> {
- const { actionId } =
- interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
- const commandName = actionId.split(':')[0];
- const handlerMethodName = actionId.split(':')[1];
- const modulePath = `./slack-command-handler/${commandName}`;
- let handler: any;
- try {
- handler = require(modulePath)(this.crowi);
- } catch (err) {
- throw new SlackCommandHandlerError(
- `No interaction.\n \`actionId: ${actionId}\``,
- );
- }
- // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
- return handler.handleInteractions(
- client,
- interactionPayload,
- interactionPayloadAccessor,
- handlerMethodName,
- respondUtil,
- );
- }
- async handleViewSubmissionRequest(
- client,
- interactionPayload: any,
- interactionPayloadAccessor: InteractionPayloadAccessor,
- respondUtil: RespondUtil,
- ): Promise<void> {
- const { callbackId } =
- interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
- const commandName = callbackId.split(':')[0];
- const handlerMethodName = callbackId.split(':')[1];
- const modulePath = `./slack-command-handler/${commandName}`;
- let handler: any;
- try {
- handler = require(modulePath)(this.crowi);
- } catch (err) {
- throw new SlackCommandHandlerError(
- `No interaction.\n \`callbackId: ${callbackId}\``,
- );
- }
- // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
- return handler.handleInteractions(
- client,
- interactionPayload,
- interactionPayloadAccessor,
- handlerMethodName,
- respondUtil,
- );
- }
- async handleEventsRequest(
- client: WebClient,
- growiBotEvent: GrowiBotEvent<any>,
- permission: EventActionsPermission,
- data?: any,
- ): Promise<void> {
- const { eventType } = growiBotEvent;
- const { channel = '' } = growiBotEvent.event; // only channelId
- if (this.linkSharedHandler.shouldHandle(eventType, permission, channel)) {
- return this.linkSharedHandler.handleEvent(client, growiBotEvent, data);
- }
- logger.error(
- `Any event actions are not permitted, or, a handler for '${eventType}' event is not implemented`,
- );
- }
- }
|