Przeglądaj źródła

imprv: Slackbot reaction to user (#4442)

* typescriptize

* refactor SlackbotError

* typescriptize

* impl overloaded handleError

* ensure getResponseUrl() not to return empty string

* replace respondIfSlackbotError with handleError

* clean code

* rename SlackError -> SlackCommandHandlerError

* clean code

* simplify error handling for SlackIntegrationService

* improve handleError

* improve default message

* simplify togetter error handling

* BugFix

* improve default responce message

* BugFix for getResponseUrl

* clean code

* autojoin when channel is public and show errors when channel is private
Yuki Takei 4 lat temu
rodzic
commit
207cc7dbef

+ 37 - 0
packages/app/src/server/models/vo/slack-command-handler-error.ts

@@ -0,0 +1,37 @@
+import ExtensibleCustomError from 'extensible-custom-error';
+
+import { RespondBodyForResponseUrl, markdownSectionBlock } from '@growi/slack';
+
+export const generateDefaultRespondBodyForInternalServerError = (message: string): RespondBodyForResponseUrl => {
+  return {
+    text: message,
+    blocks: [
+      markdownSectionBlock(`*An error occured*\n ${message}`),
+    ],
+  };
+};
+
+type Opts = {
+  responseUrl?: string,
+  respondBody?: RespondBodyForResponseUrl,
+}
+
+/**
+ * Error class for slackbot service
+ */
+export class SlackCommandHandlerError extends ExtensibleCustomError {
+
+  responseUrl?: string;
+
+  respondBody: RespondBodyForResponseUrl;
+
+  constructor(
+      message: string,
+      opts: Opts = {},
+  ) {
+    super(message);
+    this.responseUrl = opts.responseUrl;
+    this.respondBody = opts.respondBody || generateDefaultRespondBodyForInternalServerError(message);
+  }
+
+}

+ 0 - 22
packages/app/src/server/models/vo/slackbot-error.js

@@ -1,22 +0,0 @@
-/**
- * Error class for slackbot service
- */
-class SlackbotError extends Error {
-
-  constructor({
-    method, to, popupMessage, mainMessage,
-  } = {}) {
-    super();
-    this.method = method;
-    this.to = to;
-    this.popupMessage = popupMessage;
-    this.mainMessage = mainMessage;
-  }
-
-  static isSlackbotError(obj) {
-    return obj instanceof this;
-  }
-
-}
-
-module.exports = SlackbotError;

+ 5 - 15
packages/app/src/server/routes/apiv3/slack-integration.js

@@ -12,7 +12,7 @@ const {
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 const SlackAppIntegration = mongoose.model('SlackAppIntegration');
-const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
+const { handleError } = require('../../service/slack-command-handler/error-handler');
 const { checkPermission } = require('../../util/slack-integration');
 
 module.exports = (crowi) => {
@@ -195,7 +195,7 @@ module.exports = (crowi) => {
       await crowi.slackIntegrationService.handleCommandRequest(growiCommand, client, body);
     }
     catch (err) {
-      await respondIfSlackbotError(client, body, err);
+      await handleError(err, growiCommand.responseUrl);
     }
 
   }
@@ -231,20 +231,10 @@ module.exports = (crowi) => {
     try {
       switch (type) {
         case 'block_actions':
-          try {
-            await crowi.slackIntegrationService.handleBlockActionsRequest(client, interactionPayload, interactionPayloadAccessor);
-          }
-          catch (err) {
-            await respondIfSlackbotError(client, req.body, err);
-          }
+          await crowi.slackIntegrationService.handleBlockActionsRequest(client, interactionPayload, interactionPayloadAccessor);
           break;
         case 'view_submission':
-          try {
-            await crowi.slackIntegrationService.handleViewSubmissionRequest(client, interactionPayload, interactionPayloadAccessor);
-          }
-          catch (err) {
-            await respondIfSlackbotError(client, req.body, err);
-          }
+          await crowi.slackIntegrationService.handleViewSubmissionRequest(client, interactionPayload, interactionPayloadAccessor);
           break;
         default:
           break;
@@ -252,8 +242,8 @@ module.exports = (crowi) => {
     }
     catch (error) {
       logger.error(error);
+      await handleError(error, interactionPayloadAccessor.getResponseUrl());
     }
-
   }
 
   // TODO: do investigation and fix if needed GW-7519

+ 17 - 28
packages/app/src/server/service/slack-command-handler/create-page-service.js

@@ -4,7 +4,6 @@ const logger = loggerFactory('growi:service:CreatePageService');
 const { reshapeContentsBody, respond, markdownSectionBlock } = require('@growi/slack');
 const mongoose = require('mongoose');
 const pathUtils = require('growi-commons').pathUtils;
-const SlackbotError = require('../../models/vo/slackbot-error');
 
 class CreatePageService {
 
@@ -15,33 +14,23 @@ class CreatePageService {
   async createPageInGrowi(interactionPayloadAccessor, path, contentsBody) {
     const Page = this.crowi.model('Page');
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
-    try {
-      // sanitize path
-      const sanitizedPath = this.crowi.xss.process(path);
-      const normalizedPath = pathUtils.normalizePath(sanitizedPath);
-
-      // generate a dummy id because Operation to create a page needs ObjectId
-      const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
-      const page = await Page.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
-
-      // Send a message when page creation is complete
-      const growiUri = this.crowi.appService.getSiteUrl();
-      await respond(interactionPayloadAccessor.getResponseUrl(), {
-        text: 'Page has been created',
-        blocks: [
-          markdownSectionBlock(`The page <${decodeURI(`${growiUri}/${page._id} | ${decodeURI(growiUri + normalizedPath)}`)}> has been created.`),
-        ],
-      });
-    }
-    catch (err) {
-      logger.error('Failed to create page in GROWI.', err);
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Cannot create new page to existed path.',
-        mainMessage: `Cannot create new page to existed path\n *Contents* :memo:\n ${reshapedContentsBody}`,
-      });
-    }
+
+    // sanitize path
+    const sanitizedPath = this.crowi.xss.process(path);
+    const normalizedPath = pathUtils.normalizePath(sanitizedPath);
+
+    // generate a dummy id because Operation to create a page needs ObjectId
+    const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
+    const page = await Page.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
+
+    // Send a message when page creation is complete
+    const growiUri = this.crowi.appService.getSiteUrl();
+    await respond(interactionPayloadAccessor.getResponseUrl(), {
+      text: 'Page has been created',
+      blocks: [
+        markdownSectionBlock(`The page <${decodeURI(`${growiUri}/${page._id} | ${decodeURI(growiUri + normalizedPath)}`)}> has been created.`),
+      ],
+    });
   }
 
 }

+ 69 - 0
packages/app/src/server/service/slack-command-handler/error-handler.ts

@@ -0,0 +1,69 @@
+import assert from 'assert';
+import { ChatPostEphemeralResponse, WebClient } from '@slack/web-api';
+
+import { respond, RespondBodyForResponseUrl, markdownSectionBlock } from '@growi/slack';
+
+
+import { SlackCommandHandlerError } from '../../models/vo/slack-command-handler-error';
+
+function generateRespondBodyForInternalServerError(message): RespondBodyForResponseUrl {
+  return {
+    text: message,
+    blocks: [
+      markdownSectionBlock(`*GROWI Internal Server Error occured.*\n \`${message}\``),
+    ],
+  };
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+async function handleErrorWithWebClient(error: Error, client: WebClient, body: any): Promise<ChatPostEphemeralResponse> {
+
+  const isInteraction = !body.channel_id;
+
+  // this method is expected to use when system couldn't response_url
+  assert(!(error instanceof SlackCommandHandlerError) || error.responseUrl == null);
+
+  const payload = JSON.parse(body.payload);
+
+  const channel = isInteraction ? payload.channel.id : body.channel_id;
+  const user = isInteraction ? payload.user.id : body.user_id;
+
+  return client.chat.postEphemeral({
+    channel,
+    user,
+    ...generateRespondBodyForInternalServerError(error.message),
+  });
+}
+
+
+export async function handleError(error: SlackCommandHandlerError | Error, responseUrl?: string): Promise<void>;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function handleError(error: Error, client: WebClient, body: any): Promise<ChatPostEphemeralResponse>;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function handleError(error: SlackCommandHandlerError | Error, ...args: any[]): Promise<void|ChatPostEphemeralResponse> {
+
+  // handle a SlackCommandHandlerError
+  if (error instanceof SlackCommandHandlerError) {
+    const responseUrl = args[0] || error.responseUrl;
+
+    assert(responseUrl != null, 'Specify responseUrl.');
+
+    return respond(responseUrl, error.respondBody);
+  }
+
+  const secondArg = args[0];
+  assert(secondArg != null, 'Couldn\'t handle Error without the second argument.');
+
+  // handle a normal Error with response_url
+  if (typeof secondArg === 'string') {
+    const respondBody = generateRespondBodyForInternalServerError(error.message);
+    return respond(secondArg, respondBody);
+  }
+
+  assert(args[0] instanceof WebClient);
+
+  // handle with WebClient
+  return handleErrorWithWebClient(error, args[0], args[1]);
+}

+ 0 - 66
packages/app/src/server/service/slack-command-handler/respond-if-slackbot-error.js

@@ -1,66 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:service:SlackCommandHandler:slack-bot-response');
-const { markdownSectionBlock } = require('@growi/slack');
-const SlackbotError = require('../../models/vo/slackbot-error');
-
-async function respondIfSlackbotError(client, body, err) {
-  // check if the request is to /commands OR /interactions
-  const isInteraction = !body.channel_id;
-
-  // throw non-SlackbotError
-  if (!SlackbotError.isSlackbotError(err)) {
-    logger.error(`A non-SlackbotError error occured.\n${err.toString()}`);
-    throw err;
-  }
-
-  // for both postMessage and postEphemeral
-  let toChannel;
-  // for only postEphemeral
-  let toUser;
-  // decide which channel to send to
-  switch (err.to) {
-    case 'dm':
-      toChannel = isInteraction ? JSON.parse(body.payload).user.id : body.user_id;
-      toUser = toChannel;
-      break;
-    case 'channel':
-      toChannel = isInteraction ? JSON.parse(body.payload).channel.id : body.channel_id;
-      toUser = isInteraction ? JSON.parse(body.payload).user.id : body.user_id;
-      break;
-    default:
-      logger.error('The "to" property of SlackbotError must be "dm" or "channel".');
-      break;
-  }
-
-  // argumentObj object to pass to postMessage OR postEphemeral
-  let argumentsObj = {};
-  switch (err.method) {
-    case 'postMessage':
-      argumentsObj = {
-        channel: toChannel,
-        text: err.popupMessage,
-        blocks: [
-          markdownSectionBlock(err.mainMessage),
-        ],
-      };
-      break;
-    case 'postEphemeral':
-      argumentsObj = {
-        channel: toChannel,
-        user: toUser,
-        text: err.popupMessage,
-        blocks: [
-          markdownSectionBlock(err.mainMessage),
-        ],
-      };
-      break;
-    default:
-      logger.error('The "method" property of SlackbotError must be "postMessage" or "postEphemeral".');
-      break;
-  }
-
-  await client.chat[err.method](argumentsObj);
-}
-
-module.exports = { respondIfSlackbotError };

+ 0 - 1
packages/app/src/server/service/slack-command-handler/search.js

@@ -6,7 +6,6 @@ const {
   markdownSectionBlock, divider, respond, respondInChannel, replaceOriginal, deleteOriginal,
 } = require('@growi/slack');
 const { formatDistanceStrict } = require('date-fns');
-const SlackbotError = require('../../models/vo/slackbot-error');
 
 const PAGINGLIMIT = 7;
 

+ 83 - 88
packages/app/src/server/service/slack-command-handler/togetter.js

@@ -7,7 +7,7 @@ const {
 } = require('@growi/slack');
 const { parse, format } = require('date-fns');
 const axios = require('axios');
-const SlackbotError = require('../../models/vo/slackbot-error');
+const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
 module.exports = (crowi) => {
   const CreatePageService = require('./create-page-service');
@@ -37,22 +37,17 @@ module.exports = (crowi) => {
     let result = [];
     const channelId = payload.channel.id; // this must exist since the type is always block_actions
     const userChannelId = payload.user.id;
-    try {
-      // validate form
-      const { path, oldest, newest } = await this.togetterValidateForm(client, payload, interactionPayloadAccessor);
-      // get messages
-      result = await this.togetterGetMessages(client, channelId, newest, oldest);
-      // clean messages
-      const cleanedContents = await this.togetterCleanMessages(result.messages);
-
-      const contentsBody = cleanedContents.join('');
-      // create and send url message
-      await this.togetterCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userChannelId, contentsBody);
-    }
-    catch (err) {
-      logger.error('Error occured by togetter.');
-      throw err;
-    }
+
+    // validate form
+    const { path, oldest, newest } = await this.togetterValidateForm(client, payload, interactionPayloadAccessor);
+    // get messages
+    result = await this.togetterGetMessages(client, channelId, newest, oldest);
+    // clean messages
+    const cleanedContents = await this.togetterCleanMessages(result.messages);
+
+    const contentsBody = cleanedContents.join('');
+    // create and send url message
+    await this.togetterCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userChannelId, contentsBody);
   };
 
   handler.togetterValidateForm = async function(client, payload, interactionPayloadAccessor) {
@@ -60,71 +55,88 @@ module.exports = (crowi) => {
     const path = interactionPayloadAccessor.getStateValues()?.page_path.page_path.value;
     let oldest = interactionPayloadAccessor.getStateValues()?.oldest.oldest.value;
     let newest = interactionPayloadAccessor.getStateValues()?.newest.newest.value;
-    oldest = oldest.trim();
-    newest = newest.trim();
-    if (path == null) {
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Page path is required.',
-        mainMessage: 'Page path is required.',
-      });
+
+    if (oldest == null || newest == null || path == null) {
+      throw new SlackCommandHandlerError('All parameters are required. (Oldest datetime, Newst datetime and Page path)');
     }
+
     /**
      * RegExp for datetime yyyy/MM/dd-HH:mm
      * @see https://regex101.com/r/XbxdNo/1
      */
     const regexpDatetime = new RegExp(/^[12]\d\d\d\/(0[1-9]|1[012])\/(0[1-9]|[12][0-9]|3[01])-([01][0-9]|2[0123]):[0-5][0-9]$/);
 
-    if (!regexpDatetime.test(oldest)) {
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Datetime format for oldest must be yyyy/MM/dd-HH:mm',
-        mainMessage: 'Datetime format for oldest must be yyyy/MM/dd-HH:mm',
-      });
+    if (!regexpDatetime.test(oldest.trim())) {
+      throw new SlackCommandHandlerError('Datetime format for oldest must be yyyy/MM/dd-HH:mm');
     }
-    if (!regexpDatetime.test(newest)) {
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Datetime format for newest must be yyyy/MM/dd-HH:mm',
-        mainMessage: 'Datetime format for newest must be yyyy/MM/dd-HH:mm',
-      });
+    if (!regexpDatetime.test(newest.trim())) {
+      throw new SlackCommandHandlerError('Datetime format for newest must be yyyy/MM/dd-HH:mm');
     }
     oldest = parse(oldest, 'yyyy/MM/dd-HH:mm', new Date()).getTime() / 1000 + grwTzoffset;
     // + 60s in order to include messages between hh:mm.00s and hh:mm.59s
     newest = parse(newest, 'yyyy/MM/dd-HH:mm', new Date()).getTime() / 1000 + grwTzoffset + 60;
 
     if (oldest > newest) {
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Oldest datetime must be older than the newest date time.',
-        mainMessage: 'Oldest datetime must be older than the newest date time.',
-      });
+      throw new SlackCommandHandlerError('Oldest datetime must be older than the newest date time.');
     }
 
     return { path, oldest, newest };
   };
 
-  handler.togetterGetMessages = async function(client, channelId, newest, oldest) {
-    const result = await client.conversations.history({
+  async function retrieveHistory(client, channelId, newest, oldest) {
+    return client.conversations.history({
       channel: channelId,
       newest,
       oldest,
       limit: 100,
       inclusive: true,
     });
+  }
+
+  handler.togetterGetMessages = async function(client, channelId, newest, oldest) {
+    let result;
+
+    // first attempt
+    try {
+      result = await retrieveHistory(client, channelId, newest, oldest);
+    }
+    catch (err) {
+      const errorCode = err.data?.errorCode;
+
+      if (errorCode === 'not_in_channel') {
+        // join and retry
+        await client.conversations.join({
+          channel: channelId,
+        });
+        result = await retrieveHistory(client, channelId, newest, oldest);
+      }
+      else if (errorCode === 'channel_not_found') {
+
+        const message = ':cry: GROWI Bot couldn\'t get history data because *this channel was private*.'
+          + '\nPlease add GROWI bot to this channel.'
+          + '\n';
+        throw new SlackCommandHandlerError(message, {
+          respondBody: {
+            text: message,
+            blocks: [
+              markdownSectionBlock(message),
+              {
+                type: 'image',
+                image_url: 'https://user-images.githubusercontent.com/1638767/135658794-a8d2dbc8-580f-4203-b368-e74e2f3c7b3a.png',
+                alt_text: 'Add app to this channel',
+              },
+            ],
+          },
+        });
+      }
+      else {
+        throw err;
+      }
+    }
 
     // return if no message found
     if (result.messages.length === 0) {
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'No message found from togetter command. Try different datetime.',
-        mainMessage: 'No message found from togetter command. Try different datetime.',
-      });
+      throw new SlackCommandHandlerError('No message found from togetter command. Try different datetime.');
     }
     return result;
   };
@@ -157,40 +169,23 @@ module.exports = (crowi) => {
   };
 
   handler.togetterCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, userChannelId, contentsBody) {
-    try {
-      await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody);
-    }
-    catch (err) {
-      logger.error('Error occurred while creating a page.');
-      throw err;
-    }
-
-    try {
-      // send preview to dm
-      await client.chat.postMessage({
-        channel: userChannelId,
-        text: 'Preview from togetter command',
-        blocks: [
-          markdownSectionBlock('*Preview*'),
-          divider(),
-          markdownSectionBlock(contentsBody),
-          divider(),
-        ],
-      });
-      // dismiss
-      await deleteOriginal(interactionPayloadAccessor.getResponseUrl(), {
-        delete_original: true,
-      });
-    }
-    catch (err) {
-      logger.error('Error occurred while creating a page.', err);
-      throw new SlackbotError({
-        method: 'postMessage',
-        to: 'dm',
-        popupMessage: 'Error occurred while creating a page.',
-        mainMessage: 'Error occurred while creating a page.',
-      });
-    }
+    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody);
+
+    // send preview to dm
+    await client.chat.postMessage({
+      channel: userChannelId,
+      text: 'Preview from togetter command',
+      blocks: [
+        markdownSectionBlock('*Preview*'),
+        divider(),
+        markdownSectionBlock(contentsBody),
+        divider(),
+      ],
+    });
+    // dismiss
+    await deleteOriginal(interactionPayloadAccessor.getResponseUrl(), {
+      delete_original: true,
+    });
   };
 
   handler.togetterMessageBlocks = function() {

+ 28 - 42
packages/app/src/server/service/slack-integration.ts

@@ -4,7 +4,7 @@ import { IncomingWebhookSendArguments } from '@slack/webhook';
 import { ChatPostMessageArguments, WebClient } from '@slack/web-api';
 
 import {
-  generateWebClient, GrowiCommand, InteractionPayloadAccessor, markdownSectionBlock, respond, SlackbotType,
+  generateWebClient, GrowiCommand, InteractionPayloadAccessor, markdownSectionBlock, SlackbotType,
 } from '@growi/slack';
 
 import loggerFactory from '~/utils/logger';
@@ -14,6 +14,7 @@ import S2sMessage from '../models/vo/s2s-message';
 import ConfigManager from './config-manager';
 import { S2sMessagingService } from './s2s-messaging/base';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';
+import { SlackCommandHandlerError } from '../models/vo/slack-command-handler-error';
 
 
 const logger = loggerFactory('growi:service:SlackBotService');
@@ -246,72 +247,57 @@ export class SlackIntegrationService implements S2sMessageHandlable {
       handler = require(module)(this.crowi);
     }
     catch (err) {
-      logger.error(err);
-      await this.notCommand(growiCommand);
+      const text = `*No command.*\n \`command: ${growiCommand.text}\``;
+      throw new SlackCommandHandlerError(text, {
+        respondBody: {
+          text,
+          blocks: [
+            markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
+          ],
+        },
+      });
     }
 
-    try {
-      await handler.handleCommand(growiCommand, client, body);
-    }
-    catch (err) {
-      logger.error(err);
-      await this.notifyInternalError(growiCommand.responseUrl, err);
-    }
+    // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
+    return handler.handleCommand(growiCommand, client, body);
   }
 
   async handleBlockActionsRequest(client, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor): Promise<void> {
     const { actionId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const commandName = actionId.split(':')[0];
     const handlerMethodName = actionId.split(':')[1];
+
     const module = `./slack-command-handler/${commandName}`;
+
+    let handler;
     try {
-      const handler = require(module)(this.crowi);
-      await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
+      handler = require(module)(this.crowi);
     }
     catch (err) {
-      logger.error(err);
-      const responseUrl = interactionPayloadAccessor.getResponseUrl();
-      await this.notifyInternalError(responseUrl, err);
+      throw new SlackCommandHandlerError(`No interaction.\n \`actionId: ${actionId}\``);
     }
-    return;
+
+    // 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);
   }
 
   async handleViewSubmissionRequest(client, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor): Promise<void> {
     const { callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const commandName = callbackId.split(':')[0];
     const handlerMethodName = callbackId.split(':')[1];
+
     const module = `./slack-command-handler/${commandName}`;
+
+    let handler;
     try {
-      const handler = require(module)(this.crowi);
-      await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
+      handler = require(module)(this.crowi);
     }
     catch (err) {
-      logger.error(err);
-      const responseUrl = interactionPayloadAccessor.getResponseUrl();
-      await this.notifyInternalError(responseUrl, err);
+      throw new SlackCommandHandlerError(`No interaction.\n \`callbackId: ${callbackId}\``);
     }
-    return;
-  }
-
-  async notCommand(growiCommand: GrowiCommand): Promise<void> {
-    logger.error('Invalid first argument');
-    await respond(growiCommand.responseUrl, {
-      text: 'No command',
-      blocks: [
-        markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
-      ],
-    });
-    return;
-  }
 
-  async notifyInternalError(responseUrl: string, error: Error): Promise<void> {
-    await respond(responseUrl, {
-      text: 'Internal Server Error',
-      blocks: [
-        markdownSectionBlock(`*Internal Server Error*\n \`${error.message}\``),
-      ],
-    });
-    return;
+    // 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);
   }
 
 }

+ 1 - 0
packages/slack/src/index.ts

@@ -27,6 +27,7 @@ export * from './interfaces/growi-interaction-processor';
 export * from './interfaces/growi-command';
 export * from './interfaces/request-between-growi-and-proxy';
 export * from './interfaces/request-from-slack';
+export * from './interfaces/response-url';
 export * from './interfaces/slackbot-types';
 export * from './models/errors';
 export * from './middlewares/parse-slack-interaction-request';

+ 4 - 4
packages/slack/src/utils/interaction-payload-accessor.ts

@@ -1,3 +1,4 @@
+import assert from 'assert';
 import { IInteractionPayloadAccessor } from '../interfaces/request-from-slack';
 
 
@@ -27,11 +28,10 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     }
 
     const responseUrls = this.payload.response_urls;
-    if (responseUrls != null && responseUrls[0] != null) {
-      return responseUrls[0].response_url;
-    }
+    assert(responseUrls != null);
+    assert(responseUrls[0] != null);
 
-    return '';
+    return responseUrls[0].response_url;
   }
 
   getStateValues(): any | null {