| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- import {
- SlackbotType, type GrowiCommand, type GrowiBotEvent,
- } from '@growi/slack';
- import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
- import { 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 { 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 type { 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!: any;
- configManager!: ConfigManager;
- s2sMessagingService!: S2sMessagingService;
- lastLoadedAt?: Date;
- linkSharedHandler!: LinkSharedEventHandler;
- constructor(crowi) {
- this.crowi = crowi;
- this.configManager = crowi.configManager;
- 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 = !!this.configManager.getConfig('crowi', 'slackbot:currentBotType');
- return hasSlackbotType;
- }
- get isSlackLegacyConfigured(): boolean {
- // for legacy util
- const hasSlackToken = !!this.configManager.getConfig('notification', 'slack:token');
- const hasSlackIwhUrl = !!this.configManager.getConfig('notification', 'slack:incomingWebhookUrl');
- return hasSlackToken || hasSlackIwhUrl;
- }
- private isCheckTypeValid(): boolean {
- const currentBotType = this.configManager.getConfig('crowi', '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 {
- const currentBotType = this.configManager.getConfig('crowi', 'slackbot:currentBotType');
- // TODO assert currentBotType is not null and CUSTOM_WITHOUT_PROXY
- let proxyUri: string;
- switch (currentBotType) {
- case SlackbotType.OFFICIAL:
- proxyUri = OFFICIAL_SLACKBOT_PROXY_URI;
- break;
- default:
- proxyUri = this.configManager.getConfig('crowi', 'slackbot:proxyUri');
- break;
- }
- return proxyUri;
- }
- /**
- * generate WebClient instance for CUSTOM_WITHOUT_PROXY type
- */
- async generateClientForCustomBotWithoutProxy(): Promise<WebClient> {
- this.isCheckTypeValid();
- const token = this.configManager.getConfig('crowi', '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 = this.configManager.getConfig('crowi', '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('Post error', error);
- logger.debug('Sent data to slack is:', messageArgs);
- throw error;
- }
- }
- private async postMessageWithLegacyUtil(messageArgs: ChatPostMessageArguments | IncomingWebhookSendArguments): Promise<void> {
- const slackLegacyUtil = slackLegacyUtilFactory(this.configManager);
- try {
- await slackLegacyUtil.postMessage(messageArgs);
- }
- catch (error) {
- logger.debug('Post error', error);
- logger.debug('Sent data to slack is:', messageArgs);
- 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;
- 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;
- 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;
- 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`);
- }
- }
|