hakumizuki 4 лет назад
Родитель
Сommit
2f03b4cd2a

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

@@ -15,6 +15,7 @@ export * from './models/errors';
 export * from './middlewares/verify-growi-to-slack-request';
 export * from './middlewares/verify-slack-request';
 export * from './utils/block-creater';
+export * from './utils/block-kit-builder';
 export * from './utils/check-communicable';
 export * from './utils/post-ephemeral-errors';
 export * from './utils/reshape-contents-body';

+ 41 - 0
packages/slack/src/utils/block-kit-builder.ts

@@ -0,0 +1,41 @@
+import { SectionBlock, InputBlock, DividerBlock } from '@slack/types';
+
+export class BlockKitBuilder {
+
+  static generateMarkdownSectionBlock(text: string): SectionBlock {
+    return {
+      type: 'section',
+      text: {
+        type: 'mrkdwn',
+        text,
+      },
+    };
+  }
+
+  static divider(): DividerBlock {
+    return {
+      type: 'divider',
+    };
+  }
+
+  static generateInputSectionBlock(blockId: string, labelText: string, actionId: string, isMultiline: boolean, placeholder: string): InputBlock {
+    return {
+      type: 'input',
+      block_id: blockId,
+      label: {
+        type: 'plain_text',
+        text: labelText,
+      },
+      element: {
+        type: 'plain_text_input',
+        action_id: actionId,
+        multiline: isMultiline,
+        placeholder: {
+          type: 'plain_text',
+          text: placeholder,
+        },
+      },
+    };
+  }
+
+}

+ 1 - 17
src/server/routes/apiv3/slack-integration.js

@@ -104,23 +104,7 @@ module.exports = (crowi) => {
     const command = args[0];
 
     try {
-      switch (command) {
-        case 'search':
-          await crowi.slackBotService.showEphemeralSearchResults(client, body, args);
-          break;
-        case 'create':
-          await crowi.slackBotService.createModal(client, body);
-          break;
-        case 'help':
-          await crowi.slackBotService.helpCommand(client, body);
-          break;
-        case 'togetter':
-          await crowi.slackBotService.togetterCommand(client, body, args);
-          break;
-        default:
-          await crowi.slackBotService.notCommand(client, body);
-          break;
-      }
+      await crowi.slackBotService.handleCommand(command, client, body, args);
     }
     catch (error) {
       logger.error(error);

+ 52 - 0
src/server/service/slack-command-handler/create.js

@@ -0,0 +1,52 @@
+const { BlockKitBuilder: B } = require('@growi/slack');
+const logger = require('@alias/logger')('growi:service:SlackCommandHandler:create');
+
+module.exports = () => {
+  const BaseSlackCommandHandler = require('./slack-command-handler');
+  const handler = new BaseSlackCommandHandler();
+
+  handler.handleCommand = async(client, body) => {
+    try {
+      await client.views.open({
+        trigger_id: body.trigger_id,
+
+        view: {
+          type: 'modal',
+          callback_id: 'createPage',
+          title: {
+            type: 'plain_text',
+            text: 'Create Page',
+          },
+          submit: {
+            type: 'plain_text',
+            text: 'Submit',
+          },
+          close: {
+            type: 'plain_text',
+            text: 'Cancel',
+          },
+          blocks: [
+            B.generateMarkdownSectionBlock('Create new page.'),
+            B.generateInputSectionBlock('path', 'Path', 'path_input', false, '/path'),
+            B.generateInputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
+          ],
+          private_metadata: JSON.stringify({ channelId: body.channel_id }),
+        },
+      });
+    }
+    catch (err) {
+      logger.error('Failed to create a page.');
+      await client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: 'Failed To Create',
+        blocks: [
+          B.generateMarkdownSectionBlock(`*Failed to create new page.*\n ${err}`),
+        ],
+      });
+      throw err;
+    }
+  };
+
+  return handler;
+};

+ 20 - 0
src/server/service/slack-command-handler/help.js

@@ -0,0 +1,20 @@
+const { BlockKitBuilder: B } = require('@growi/slack');
+
+module.exports = () => {
+  const BaseSlackCommandHandler = require('./slack-command-handler');
+  const handler = new BaseSlackCommandHandler();
+
+  handler.handleCommand = (client, body) => {
+    const message = '*Help*\n growi-bot usage\n `/growi [command] [args]`\n\n Create new page\n `create`\n\n Search pages\n `search [keyword]`';
+    client.chat.postEphemeral({
+      channel: body.channel_id,
+      user: body.user_id,
+      text: 'Help',
+      blocks: [
+        B.generateMarkdownSectionBlock(message),
+      ],
+    });
+  };
+
+  return handler;
+};

+ 250 - 0
src/server/service/slack-command-handler/search.js

@@ -0,0 +1,250 @@
+const logger = require('@alias/logger')('growi:service:SlackCommandHandler:search');
+
+const { BlockKitBuilder: B } = require('@growi/slack');
+const { formatDistanceStrict } = require('date-fns');
+
+const PAGINGLIMIT = 10;
+
+module.exports = (crowi) => {
+  const BaseSlackCommandHandler = require('./slack-command-handler');
+  const handler = new BaseSlackCommandHandler(crowi);
+
+  handler.handleCommand = async function(client, body, args) {
+    let searchResult;
+    try {
+      searchResult = await this.retrieveSearchResults(client, body, args);
+    }
+    catch (err) {
+      logger.error('Failed to get search results.', err);
+      await client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: 'Failed To Search',
+        blocks: [
+          B.generateMarkdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
+        ],
+      });
+      throw new Error('/growi command:search: Failed to search');
+    }
+
+    const appUrl = this.crowi.appService.getSiteUrl();
+    const appTitle = this.crowi.appService.getAppTitle();
+
+    const {
+      pages, offset, resultsTotal,
+    } = searchResult;
+
+    const keywords = this.getKeywords(args);
+
+
+    let searchResultsDesc;
+
+    switch (resultsTotal) {
+      case 1:
+        searchResultsDesc = `*${resultsTotal}* page is found.`;
+        break;
+
+      default:
+        searchResultsDesc = `*${resultsTotal}* pages are found.`;
+        break;
+    }
+
+
+    const contextBlock = {
+      type: 'context',
+      elements: [
+        {
+          type: 'mrkdwn',
+          text: `keyword(s) : *"${keywords}"*  |  Current: ${offset + 1} - ${offset + pages.length}  |  Total ${resultsTotal} pages`,
+        },
+      ],
+    };
+
+    const now = new Date();
+    const blocks = [
+      B.generateMarkdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
+      contextBlock,
+      { type: 'divider' },
+      // create an array by map and extract
+      ...pages.map((page) => {
+        const { path, updatedAt, commentCount } = page;
+        // generate URL
+        const url = new URL(path, appUrl);
+        const { href, pathname } = url;
+
+        return {
+          type: 'section',
+          text: {
+            type: 'mrkdwn',
+            text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
+              + `\n    Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
+          },
+          accessory: {
+            type: 'button',
+            action_id: 'shareSingleSearchResult',
+            text: {
+              type: 'plain_text',
+              text: 'Share',
+            },
+            value: JSON.stringify({ page, href, pathname }),
+          },
+        };
+      }),
+      { type: 'divider' },
+      contextBlock,
+    ];
+
+    // DEFAULT show "Share" button
+    // const actionBlocks = {
+    //   type: 'actions',
+    //   elements: [
+    //     {
+    //       type: 'button',
+    //       text: {
+    //         type: 'plain_text',
+    //         text: 'Share',
+    //       },
+    //       style: 'primary',
+    //       action_id: 'shareSearchResults',
+    //     },
+    //   ],
+    // };
+    const actionBlocks = {
+      type: 'actions',
+      elements: [
+        {
+          type: 'button',
+          text: {
+            type: 'plain_text',
+            text: 'Dismiss',
+          },
+          style: 'danger',
+          action_id: 'dismissSearchResults',
+        },
+      ],
+    };
+    // show "Next" button if next page exists
+    if (resultsTotal > offset + PAGINGLIMIT) {
+      actionBlocks.elements.unshift(
+        {
+          type: 'button',
+          text: {
+            type: 'plain_text',
+            text: 'Next',
+          },
+          action_id: 'showNextResults',
+          value: JSON.stringify({ offset, body, args }),
+        },
+      );
+    }
+    blocks.push(actionBlocks);
+
+    try {
+      await client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: 'Successed To Search',
+        blocks,
+      });
+    }
+    catch (err) {
+      logger.error('Failed to post ephemeral message.', err);
+      await client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: 'Failed to post ephemeral message.',
+        blocks: [
+          B.generateMarkdownSectionBlock(err.toString()),
+        ],
+      });
+      throw new Error(err);
+    }
+  };
+
+  handler.retrieveSearchResults = async function(client, body, args, offset = 0) {
+    const firstKeyword = args[1];
+    if (firstKeyword == null) {
+      client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: 'Input keywords',
+        blocks: [
+          B.generateMarkdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
+        ],
+      });
+      return;
+    }
+
+    const keywords = this.getKeywords(args);
+
+    const { searchService } = this.crowi;
+    const options = { limit: 10, offset };
+    const results = await searchService.searchKeyword(keywords, null, {}, options);
+    const resultsTotal = results.meta.total;
+
+    // no search results
+    if (results.data.length === 0) {
+      logger.info(`No page found with "${keywords}"`);
+      client.chat.postEphemeral({
+        channel: body.channel_id,
+        user: body.user_id,
+        text: `No page found with "${keywords}"`,
+        blocks: [
+          B.generateMarkdownSectionBlock(`*No page that matches your keyword(s) "${keywords}".*`),
+          B.generateMarkdownSectionBlock(':mag: *Help: Searching*'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`word1` `word2` (divide with space) \n Search pages that include both word1, word2 in the title or body'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`"This is GROWI"` (surround with double quotes) \n Search pages that include the phrase "This is GROWI"'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`-keyword` \n Exclude pages that include keyword in the title or body'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`prefix:/user/` \n Search only the pages that the title start with /user/'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`-prefix:/user/` \n Exclude the pages that the title start with /user/'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`tag:wiki` \n Search for pages with wiki tag'),
+          B.divider(),
+          B.generateMarkdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
+        ],
+      });
+      return { pages: [] };
+    }
+
+    const pages = results.data.map((data) => {
+      const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
+      return { path, updatedAt, commentCount };
+    });
+
+    return {
+      pages, offset, resultsTotal,
+    };
+  };
+
+  handler.getKeywords = function(args) {
+    const keywordsArr = args.slice(1);
+    const keywords = keywordsArr.join(' ');
+    return keywords;
+  };
+
+  handler.appendSpeechBaloon = function(mrkdwn, commentCount) {
+    return (commentCount != null && commentCount > 0)
+      ? `${mrkdwn}   :speech_balloon: ${commentCount}`
+      : mrkdwn;
+  };
+
+  handler.generatePageLinkMrkdwn = function(pathname, href) {
+    return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
+  };
+
+  handler.generateLastUpdateMrkdwn = function(updatedAt, baseDate) {
+    if (updatedAt != null) {
+      // cast to date
+      const date = new Date(updatedAt);
+      return formatDistanceStrict(date, baseDate);
+    }
+    return '';
+  };
+
+  return handler;
+};

+ 15 - 0
src/server/service/slack-command-handler/slack-command-handler.js

@@ -0,0 +1,15 @@
+// Any slack command handler should inherit BaseSlackCommandHandler
+class BaseSlackCommandHandler {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  /**
+   * Handle /commands endpoint
+   */
+  handleCommand(client, body, ...opt) { throw new Error('Implement this') }
+
+}
+
+module.exports = BaseSlackCommandHandler;

+ 20 - 281
src/server/service/slackbot.js

@@ -2,11 +2,10 @@
 const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 const axios = require('axios');
-const { formatDistanceStrict } = require('date-fns');
-
-const PAGINGLIMIT = 10;
 
+const { BlockKitBuilder: B } = require('@growi/slack');
 const { reshapeContentsBody } = require('@growi/slack');
+const { formatDistanceStrict } = require('date-fns');
 
 const S2sMessage = require('../models/vo/s2s-message');
 const S2sMessageHandlable = require('./s2s-messaging/handlable');
@@ -67,6 +66,20 @@ class SlackBotService extends S2sMessageHandlable {
     }
   }
 
+  /**
+   * Handle /commands endpoint
+   */
+  async handleCommand(command, client, body, ...opt) {
+    const module = `./slack-command-handler/${command}`;
+    try {
+      const handler = require(module)(this.crowi);
+      await handler.handleCommand(client, body, ...opt);
+    }
+    catch (err) {
+      this.notCommand(client, body);
+    }
+  }
+
   async notCommand(client, body) {
     logger.error('Invalid first argument');
     client.chat.postEphemeral({
@@ -74,20 +87,7 @@ class SlackBotService extends S2sMessageHandlable {
       user: body.user_id,
       text: 'No command',
       blocks: [
-        this.generateMarkdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
-      ],
-    });
-    return;
-  }
-
-  async helpCommand(client, body) {
-    const message = '*Help*\n growi-bot usage\n `/growi [command] [args]`\n\n Create new page\n `create`\n\n Search pages\n `search [keyword]`';
-    client.chat.postEphemeral({
-      channel: body.channel_id,
-      user: body.user_id,
-      text: 'Help',
-      blocks: [
-        this.generateMarkdownSectionBlock(message),
+        B.generateMarkdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
       ],
     });
     return;
@@ -110,72 +110,6 @@ class SlackBotService extends S2sMessageHandlable {
     return;
   }
 
-  getKeywords(args) {
-    const keywordsArr = args.slice(1);
-    const keywords = keywordsArr.join(' ');
-    return keywords;
-  }
-
-  async retrieveSearchResults(client, body, args, offset = 0) {
-    const firstKeyword = args[1];
-    if (firstKeyword == null) {
-      client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: 'Input keywords',
-        blocks: [
-          this.generateMarkdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
-        ],
-      });
-      return;
-    }
-
-    const keywords = this.getKeywords(args);
-
-    const { searchService } = this.crowi;
-    const options = { limit: 10, offset };
-    const results = await searchService.searchKeyword(keywords, null, {}, options);
-    const resultsTotal = results.meta.total;
-
-    // no search results
-    if (results.data.length === 0) {
-      logger.info(`No page found with "${keywords}"`);
-      client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: `No page found with "${keywords}"`,
-        blocks: [
-          this.generateMarkdownSectionBlock(`*No page that matches your keyword(s) "${keywords}".*`),
-          this.generateMarkdownSectionBlock(':mag: *Help: Searching*'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`word1` `word2` (divide with space) \n Search pages that include both word1, word2 in the title or body'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`"This is GROWI"` (surround with double quotes) \n Search pages that include the phrase "This is GROWI"'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`-keyword` \n Exclude pages that include keyword in the title or body'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`prefix:/user/` \n Search only the pages that the title start with /user/'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`-prefix:/user/` \n Exclude the pages that the title start with /user/'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`tag:wiki` \n Search for pages with wiki tag'),
-          this.divider(),
-          this.generateMarkdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
-        ],
-      });
-      return { pages: [] };
-    }
-
-    const pages = results.data.map((data) => {
-      const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
-      return { path, updatedAt, commentCount };
-    });
-
-    return {
-      pages, offset, resultsTotal,
-    };
-  }
-
   generatePageLinkMrkdwn(pathname, href) {
     return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
   }
@@ -195,6 +129,7 @@ class SlackBotService extends S2sMessageHandlable {
     return '';
   }
 
+
   async shareSinglePage(client, payload) {
     const { channel, user, actions } = payload;
 
@@ -214,7 +149,7 @@ class SlackBotService extends S2sMessageHandlable {
       channel: channelId,
       blocks: [
         { type: 'divider' },
-        this.generateMarkdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
+        B.generateMarkdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
         {
           type: 'context',
           elements: [
@@ -236,202 +171,6 @@ class SlackBotService extends S2sMessageHandlable {
     });
   }
 
-  async showEphemeralSearchResults(client, body, args, offsetNum) {
-
-    let searchResult;
-    try {
-      searchResult = await this.retrieveSearchResults(client, body, args, offsetNum);
-    }
-    catch (err) {
-      logger.error('Failed to get search results.', err);
-      await client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: 'Failed To Search',
-        blocks: [
-          this.generateMarkdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
-        ],
-      });
-      throw new Error('/growi command:search: Failed to search');
-    }
-
-    const appUrl = this.crowi.appService.getSiteUrl();
-    const appTitle = this.crowi.appService.getAppTitle();
-
-    const {
-      pages, offset, resultsTotal,
-    } = searchResult;
-
-    const keywords = this.getKeywords(args);
-
-
-    let searchResultsDesc;
-
-    switch (resultsTotal) {
-      case 1:
-        searchResultsDesc = `*${resultsTotal}* page is found.`;
-        break;
-
-      default:
-        searchResultsDesc = `*${resultsTotal}* pages are found.`;
-        break;
-    }
-
-
-    const contextBlock = {
-      type: 'context',
-      elements: [
-        {
-          type: 'mrkdwn',
-          text: `keyword(s) : *"${keywords}"*  |  Current: ${offset + 1} - ${offset + pages.length}  |  Total ${resultsTotal} pages`,
-        },
-      ],
-    };
-
-    const now = new Date();
-    const blocks = [
-      this.generateMarkdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
-      contextBlock,
-      { type: 'divider' },
-      // create an array by map and extract
-      ...pages.map((page) => {
-        const { path, updatedAt, commentCount } = page;
-        // generate URL
-        const url = new URL(path, appUrl);
-        const { href, pathname } = url;
-
-        return {
-          type: 'section',
-          text: {
-            type: 'mrkdwn',
-            text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
-              + `\n    Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
-          },
-          accessory: {
-            type: 'button',
-            action_id: 'shareSingleSearchResult',
-            text: {
-              type: 'plain_text',
-              text: 'Share',
-            },
-            value: JSON.stringify({ page, href, pathname }),
-          },
-        };
-      }),
-      { type: 'divider' },
-      contextBlock,
-    ];
-
-    // DEFAULT show "Share" button
-    // const actionBlocks = {
-    //   type: 'actions',
-    //   elements: [
-    //     {
-    //       type: 'button',
-    //       text: {
-    //         type: 'plain_text',
-    //         text: 'Share',
-    //       },
-    //       style: 'primary',
-    //       action_id: 'shareSearchResults',
-    //     },
-    //   ],
-    // };
-    const actionBlocks = {
-      type: 'actions',
-      elements: [
-        {
-          type: 'button',
-          text: {
-            type: 'plain_text',
-            text: 'Dismiss',
-          },
-          style: 'danger',
-          action_id: 'dismissSearchResults',
-        },
-      ],
-    };
-    // show "Next" button if next page exists
-    if (resultsTotal > offset + PAGINGLIMIT) {
-      actionBlocks.elements.unshift(
-        {
-          type: 'button',
-          text: {
-            type: 'plain_text',
-            text: 'Next',
-          },
-          action_id: 'showNextResults',
-          value: JSON.stringify({ offset, body, args }),
-        },
-      );
-    }
-    blocks.push(actionBlocks);
-
-    try {
-      await client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: 'Successed To Search',
-        blocks,
-      });
-    }
-    catch (err) {
-      logger.error('Failed to post ephemeral message.', err);
-      await client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: 'Failed to post ephemeral message.',
-        blocks: [
-          this.generateMarkdownSectionBlock(err.toString()),
-        ],
-      });
-      throw new Error(err);
-    }
-  }
-
-  async createModal(client, body) {
-    try {
-      await client.views.open({
-        trigger_id: body.trigger_id,
-
-        view: {
-          type: 'modal',
-          callback_id: 'createPage',
-          title: {
-            type: 'plain_text',
-            text: 'Create Page',
-          },
-          submit: {
-            type: 'plain_text',
-            text: 'Submit',
-          },
-          close: {
-            type: 'plain_text',
-            text: 'Cancel',
-          },
-          blocks: [
-            this.generateMarkdownSectionBlock('Create new page.'),
-            this.generateInputSectionBlock('path', 'Path', 'path_input', false, '/path'),
-            this.generateInputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
-          ],
-          private_metadata: JSON.stringify({ channelId: body.channel_id }),
-        },
-      });
-    }
-    catch (err) {
-      logger.error('Failed to create a page.');
-      await client.chat.postEphemeral({
-        channel: body.channel_id,
-        user: body.user_id,
-        text: 'Failed To Create',
-        blocks: [
-          this.generateMarkdownSectionBlock(`*Failed to create new page.*\n ${err}`),
-        ],
-      });
-      throw err;
-    }
-  }
-
   // Submit action in create Modal
   async createPageInGrowi(client, payload) {
     const Page = this.crowi.model('Page');
@@ -461,7 +200,7 @@ class SlackBotService extends S2sMessageHandlable {
       client.chat.postMessage({
         channel: payload.user.id,
         blocks: [
-          this.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
+          B.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
       });
       logger.error('Failed to create page in GROWI.');
       throw err;