Просмотр исходного кода

Merge pull request #4016 from weseek/feat/GW-6710-slackbot-togetter-command-show-messages-with-mocks

Feat/gw 6710 slackbot togetter command show messages with mocks
Haku Mizuki 4 лет назад
Родитель
Сommit
f511769b85

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

@@ -14,7 +14,6 @@ export * from './interfaces/request-from-slack';
 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';

+ 0 - 31
packages/slack/src/utils/block-creater.ts

@@ -1,31 +0,0 @@
-import { SectionBlock, InputBlock } from '@slack/types';
-
-export const generateMarkdownSectionBlock = (blocks:string):SectionBlock => {
-  return {
-    type: 'section',
-    text: {
-      type: 'mrkdwn',
-      text: blocks,
-    },
-  };
-};
-
-export const 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,
-      },
-    },
-  };
-};

+ 92 - 32
packages/slack/src/utils/block-kit-builder.ts

@@ -1,41 +1,101 @@
-import { SectionBlock, InputBlock, DividerBlock } from '@slack/types';
+import {
+  SectionBlock, InputBlock, DividerBlock, ActionsBlock,
+  Button, Overflow, Datepicker, Select, RadioButtons, Checkboxes, Action, MultiSelect, PlainTextInput, Option,
+} from '@slack/types';
 
-export class BlockKitBuilder {
 
-  static generateMarkdownSectionBlock(text: string): SectionBlock {
-    return {
-      type: 'section',
-      text: {
-        type: 'mrkdwn',
-        text,
-      },
-    };
-  }
+export function divider(): DividerBlock {
+  return {
+    type: 'divider',
+  };
+}
 
-  static divider(): DividerBlock {
-    return {
-      type: 'divider',
-    };
-  }
+export function markdownSectionBlock(text: string): SectionBlock {
+  return {
+    type: 'section',
+    text: {
+      type: 'mrkdwn',
+      text,
+    },
+  };
+}
 
-  static generateInputSectionBlock(blockId: string, labelText: string, actionId: string, isMultiline: boolean, placeholder: string): InputBlock {
-    return {
-      type: 'input',
-      block_id: blockId,
-      label: {
+export function inputSectionBlock(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: labelText,
-      },
-      element: {
-        type: 'plain_text_input',
-        action_id: actionId,
-        multiline: isMultiline,
-        placeholder: {
-          type: 'plain_text',
-          text: placeholder,
-        },
+        text: placeholder,
       },
-    };
+    },
+  };
+}
+
+export function actionsBlock(...elements: (Button | Overflow | Datepicker | Select | RadioButtons | Checkboxes | Action)[]): ActionsBlock {
+  return {
+    type: 'actions',
+    elements: [
+      ...elements,
+    ],
+  };
+}
+
+export function inputBlock(
+    element: Select | MultiSelect | Datepicker | PlainTextInput | RadioButtons | Checkboxes, blockId: string, labelText: string,
+): InputBlock {
+  return {
+    type: 'input',
+    block_id: blockId,
+    element,
+    label: {
+      type: 'plain_text',
+      text: labelText,
+    },
+  };
+}
+
+/**
+ * Button element
+ * https://api.slack.com/reference/block-kit/block-elements#button
+ */
+export function buttonElement(text: string, actionId: string, style?: string): Button {
+  const button: Button = {
+    type: 'button',
+    text: {
+      type: 'plain_text',
+      text,
+    },
+    action_id: actionId,
+  };
+  if (style === 'primary' || style === 'danger') {
+    button.style = style;
   }
+  return button;
+}
 
+/**
+ * Option object
+ * https://api.slack.com/reference/block-kit/composition-objects#option
+ */
+export function checkboxesElementOption(text: string, description: string, value: string): Option {
+  return {
+    text: {
+      type: 'mrkdwn',
+      text,
+    },
+    description: {
+      type: 'plain_text',
+      text: description,
+    },
+    value,
+  };
 }

+ 3 - 3
packages/slack/src/utils/post-ephemeral-errors.ts

@@ -1,6 +1,6 @@
 import { WebAPICallResult } from '@slack/web-api';
 
-import { generateMarkdownSectionBlock } from './block-creater';
+import { markdownSectionBlock } from './block-kit-builder';
 import { generateWebClient } from './webclient-factory';
 
 export const postEphemeralErrors = async(
@@ -19,7 +19,7 @@ export const postEphemeralErrors = async(
       channel: channelId,
       user: userId,
       blocks: [
-        generateMarkdownSectionBlock('*Error occured:*'),
+        markdownSectionBlock('*Error occured:*'),
         ...rejectedResults.map((rejectedResult) => {
           const reason = rejectedResult.reason.toString();
           const resData = rejectedResult.reason.response?.data;
@@ -30,7 +30,7 @@ export const postEphemeralErrors = async(
             errorMessage += `\n  Cause: ${resDataMessage}`;
           }
 
-          return generateMarkdownSectionBlock(errorMessage);
+          return markdownSectionBlock(errorMessage);
         }),
       ],
     });

+ 5 - 5
packages/slackbot-proxy/src/controllers/slack.ts

@@ -7,7 +7,7 @@ import axios from 'axios';
 import { WebAPICallResult } from '@slack/web-api';
 
 import {
-  generateMarkdownSectionBlock, GrowiCommand, parseSlashCommand, postEphemeralErrors, verifySlackRequest,
+  markdownSectionBlock, GrowiCommand, parseSlashCommand, postEphemeralErrors, verifySlackRequest,
 } from '@growi/slack';
 
 import { Relation } from '~/entities/relation';
@@ -139,8 +139,8 @@ export class SlackCtrl {
     if (relations.length === 0) {
       return res.json({
         blocks: [
-          generateMarkdownSectionBlock('*No relation found.*'),
-          generateMarkdownSectionBlock('Run `/growi register` first.'),
+          markdownSectionBlock('*No relation found.*'),
+          markdownSectionBlock('Run `/growi register` first.'),
         ],
       });
     }
@@ -149,8 +149,8 @@ export class SlackCtrl {
     if (growiCommand.growiCommandType === 'status') {
       return res.json({
         blocks: [
-          generateMarkdownSectionBlock('*Found Relations to GROWI.*'),
-          ...relations.map(relation => generateMarkdownSectionBlock(`GROWI url: ${relation.growiUri}`)),
+          markdownSectionBlock('*Found Relations to GROWI.*'),
+          ...relations.map(relation => markdownSectionBlock(`GROWI url: ${relation.growiUri}`)),
         ],
       });
     }

+ 8 - 8
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,6 +1,6 @@
 import { Inject, Service } from '@tsed/di';
 import { WebClient, LogLevel, Block } from '@slack/web-api';
-import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
+import { markdownSectionBlock, inputSectionBlock, GrowiCommand } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { OrderRepository } from '~/repositories/order';
@@ -40,9 +40,9 @@ export class RegisterService implements GrowiCommandProcessor {
         private_metadata: JSON.stringify({ channel: body.channel_name }),
 
         blocks: [
-          generateInputSectionBlock('growiUrl', 'GROWI domain', 'contents_input', false, 'https://example.com'),
-          generateInputSectionBlock('tokenPtoG', 'Access Token Proxy to GROWI', 'contents_input', false, 'jBMZvpk.....'),
-          generateInputSectionBlock('tokenGtoP', 'Access Token GROWI to Proxy', 'contents_input', false, 'sdg15av.....'),
+          inputSectionBlock('growiUrl', 'GROWI domain', 'contents_input', false, 'https://example.com'),
+          inputSectionBlock('tokenPtoG', 'Access Token Proxy to GROWI', 'contents_input', false, 'jBMZvpk.....'),
+          inputSectionBlock('tokenGtoP', 'Access Token GROWI to Proxy', 'contents_input', false, 'sdg15av.....'),
         ],
       },
     });
@@ -81,7 +81,7 @@ export class RegisterService implements GrowiCommandProcessor {
     catch (error) {
       const invalidErrorMsg = 'Please enter a valid URL';
       const blocks = [
-        generateMarkdownSectionBlock(invalidErrorMsg),
+        markdownSectionBlock(invalidErrorMsg),
       ];
       await this.replyToSlack(client, channel, payload.user.id, 'Invalid URL', blocks);
       throw new InvalidUrlError(growiUrl);
@@ -105,7 +105,7 @@ export class RegisterService implements GrowiCommandProcessor {
 
     if (isOfficialMode) {
       const blocks = [
-        generateMarkdownSectionBlock('Successfully registered with the proxy! Please check test connection in your GROWI'),
+        markdownSectionBlock('Successfully registered with the proxy! Please check test connection in your GROWI'),
       ];
       await this.replyToSlack(client, channel, payload.user.id, 'Proxy URL', blocks);
       return;
@@ -113,8 +113,8 @@ export class RegisterService implements GrowiCommandProcessor {
     }
 
     const blocks = [
-      generateMarkdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
-      generateMarkdownSectionBlock(`Proxy URL: ${serverUri}`),
+      markdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
+      markdownSectionBlock(`Proxy URL: ${serverUri}`),
     ];
     await this.replyToSlack(client, channel, payload.user.id, 'Proxy URL', blocks);
     return;

+ 3 - 3
packages/slackbot-proxy/src/services/UnregisterService.ts

@@ -1,6 +1,6 @@
 import { Inject, Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
-import { GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
+import { GrowiCommand, markdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { RelationRepository } from '~/repositories/relation';
@@ -38,7 +38,7 @@ export class UnregisterService implements GrowiCommandProcessor {
         private_metadata: JSON.stringify({ channel: body.channel_name, growiUrls }),
 
         blocks: [
-          ...growiUrls.map(growiCommandArg => generateMarkdownSectionBlock(`GROWI url: ${growiCommandArg}.`)),
+          ...growiUrls.map(growiCommandArg => markdownSectionBlock(`GROWI url: ${growiCommandArg}.`)),
         ],
       },
     });
@@ -63,7 +63,7 @@ export class UnregisterService implements GrowiCommandProcessor {
       // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
       text: 'Delete Relations',
       blocks: [
-        generateMarkdownSectionBlock(`Deleted ${deleteResult.affected} Relations.`),
+        markdownSectionBlock(`Deleted ${deleteResult.affected} Relations.`),
       ],
     });
 

+ 12 - 0
src/server/routes/apiv3/slack-integration.js

@@ -148,6 +148,18 @@ module.exports = (crowi) => {
         await crowi.slackBotService.showEphemeralSearchResults(client, body, args, newOffset);
         break;
       }
+      case 'togetterShowMore': {
+        console.log('Show more here');
+        break;
+      }
+      case 'togetterCreatePage': {
+        console.log('Create page and delete the original message here');
+        break;
+      }
+      case 'togetterCancelPageCreation': {
+        console.log('Cancel here');
+        break;
+      }
       case 'showMoreTogetterResults': {
         const parsedValue = JSON.parse(payload.actions[0].value);
 

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

@@ -1,4 +1,4 @@
-const { BlockKitBuilder: B } = require('@growi/slack');
+const { markdownSectionBlock, inputSectionBlock } = require('@growi/slack');
 const logger = require('@alias/logger')('growi:service:SlackCommandHandler:create');
 
 module.exports = () => {
@@ -26,9 +26,9 @@ module.exports = () => {
             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...'),
+            markdownSectionBlock('Create new page.'),
+            inputSectionBlock('path', 'Path', 'path_input', false, '/path'),
+            inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
           ],
           private_metadata: JSON.stringify({ channelId: body.channel_id }),
         },
@@ -41,7 +41,7 @@ module.exports = () => {
         user: body.user_id,
         text: 'Failed To Create',
         blocks: [
-          B.generateMarkdownSectionBlock(`*Failed to create new page.*\n ${err}`),
+          markdownSectionBlock(`*Failed to create new page.*\n ${err}`),
         ],
       });
       throw err;

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

@@ -1,4 +1,4 @@
-const { BlockKitBuilder: B } = require('@growi/slack');
+const { markdownSectionBlock } = require('@growi/slack');
 
 module.exports = () => {
   const BaseSlackCommandHandler = require('./slack-command-handler');
@@ -11,7 +11,7 @@ module.exports = () => {
       user: body.user_id,
       text: 'Help',
       blocks: [
-        B.generateMarkdownSectionBlock(message),
+        markdownSectionBlock(message),
       ],
     });
   };

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

@@ -1,6 +1,6 @@
 const logger = require('@alias/logger')('growi:service:SlackCommandHandler:search');
 
-const { BlockKitBuilder: B } = require('@growi/slack');
+const { markdownSectionBlock, divider } = require('@growi/slack');
 const { formatDistanceStrict } = require('date-fns');
 
 const PAGINGLIMIT = 10;
@@ -21,7 +21,7 @@ module.exports = (crowi) => {
         user: body.user_id,
         text: 'Failed To Search',
         blocks: [
-          B.generateMarkdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
+          markdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
         ],
       });
       throw new Error('/growi command:search: Failed to search');
@@ -62,7 +62,7 @@ module.exports = (crowi) => {
 
     const now = new Date();
     const blocks = [
-      B.generateMarkdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
+      markdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
       contextBlock,
       { type: 'divider' },
       // create an array by map and extract
@@ -154,7 +154,7 @@ module.exports = (crowi) => {
         user: body.user_id,
         text: 'Failed to post ephemeral message.',
         blocks: [
-          B.generateMarkdownSectionBlock(err.toString()),
+          markdownSectionBlock(err.toString()),
         ],
       });
       throw new Error(err);
@@ -169,7 +169,7 @@ module.exports = (crowi) => {
         user: body.user_id,
         text: 'Input keywords',
         blocks: [
-          B.generateMarkdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
+          markdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
         ],
       });
       return;
@@ -190,22 +190,22 @@ module.exports = (crowi) => {
         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'),
+          markdownSectionBlock(`*No page that matches your keyword(s) "${keywords}".*`),
+          markdownSectionBlock(':mag: *Help: Searching*'),
+          divider(),
+          markdownSectionBlock('`word1` `word2` (divide with space) \n Search pages that include both word1, word2 in the title or body'),
+          divider(),
+          markdownSectionBlock('`"This is GROWI"` (surround with double quotes) \n Search pages that include the phrase "This is GROWI"'),
+          divider(),
+          markdownSectionBlock('`-keyword` \n Exclude pages that include keyword in the title or body'),
+          divider(),
+          markdownSectionBlock('`prefix:/user/` \n Search only the pages that the title start with /user/'),
+          divider(),
+          markdownSectionBlock('`-prefix:/user/` \n Exclude the pages that the title start with /user/'),
+          divider(),
+          markdownSectionBlock('`tag:wiki` \n Search for pages with wiki tag'),
+          divider(),
+          markdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
         ],
       });
       return { pages: [] };

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

@@ -0,0 +1,70 @@
+const {
+  inputBlock, actionsBlock, buttonElement, checkboxesElementOption,
+} = require('@growi/slack');
+
+module.exports = () => {
+  const BaseSlackCommandHandler = require('./slack-command-handler');
+  const handler = new BaseSlackCommandHandler();
+
+  handler.handleCommand = async function(client, body, args, limit = 10) {
+    // TODO GW-6721 Get the time from args
+    const reusult = await client.conversations.history({
+      channel: body.channel_id,
+      limit,
+    });
+    console.log(reusult);
+    // Return Checkbox Message
+    client.chat.postEphemeral({
+      channel: body.channel_id,
+      user: body.user_id,
+      text: 'Select messages to use.',
+      blocks: this.togetterMessageBlocks(),
+    });
+    return;
+  };
+
+  handler.togetterMessageBlocks = function() {
+    return [
+      inputBlock(this.togetterCheckboxesElement(), 'selected_messages', 'Select massages to use.'),
+      actionsBlock(buttonElement('Show more', 'togetterShowMore')),
+      inputBlock(this.togetterInputBlockElement('page_path', '/'), 'page_path', 'Page path'),
+      actionsBlock(buttonElement('Cancel', 'togetterCancelPageCreation'), buttonElement('Create page', 'togetterCreatePage', 'primary')),
+    ];
+  };
+
+  handler.togetterCheckboxesElement = function() {
+    return {
+      type: 'checkboxes',
+      options: this.togetterCheckboxesElementOptions(),
+      action_id: 'checkboxes_changed',
+    };
+  };
+
+  handler.togetterCheckboxesElementOptions = function() {
+    // increment options with results from conversations.history
+    const options = [];
+    // temporary code
+    for (let i = 0; i < 10; i++) {
+      const option = checkboxesElementOption('*username*  12:00PM', 'sample slack messages ... :star:', `selected-${i}`);
+      options.push(option);
+    }
+    return options;
+  };
+
+  /**
+   * Plain-text input element
+   * https://api.slack.com/reference/block-kit/block-elements#input
+   */
+  handler.togetterInputBlockElement = function(actionId, placeholderText = 'Write something ...') {
+    return {
+      type: 'plain_text_input',
+      placeholder: {
+        type: 'plain_text',
+        text: placeholderText,
+      },
+      action_id: actionId,
+    };
+  };
+
+  return handler;
+};

+ 4 - 24
src/server/service/slackbot.js

@@ -3,7 +3,7 @@ const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 const axios = require('axios');
 
-const { BlockKitBuilder: B } = require('@growi/slack');
+const { markdownSectionBlock } = require('@growi/slack');
 const { reshapeContentsBody } = require('@growi/slack');
 const { formatDistanceStrict } = require('date-fns');
 
@@ -87,27 +87,7 @@ class SlackBotService extends S2sMessageHandlable {
       user: body.user_id,
       text: 'No command',
       blocks: [
-        B.generateMarkdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
-      ],
-    });
-    return;
-  }
-
-  async togetterCommand(client, body, args, limit = 10) {
-    // TODO GW-6721 Get the time from args
-    const reusult = await client.conversations.history({
-      channel: body.channel_id,
-      limit,
-    });
-    console.log(reusult);
-    // TODO GW-6712 display checkbox using result
-    const message = '*togetterCommand*';
-    client.chat.postEphemeral({
-      channel: body.channel_id,
-      user: body.user_id,
-      text: 'togetter',
-      blocks: [
-        this.generateMarkdownSectionBlock(message),
+        markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
       ],
     });
     return;
@@ -152,7 +132,7 @@ class SlackBotService extends S2sMessageHandlable {
       channel: channelId,
       blocks: [
         { type: 'divider' },
-        B.generateMarkdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
+        markdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
         {
           type: 'context',
           elements: [
@@ -203,7 +183,7 @@ class SlackBotService extends S2sMessageHandlable {
       client.chat.postMessage({
         channel: payload.user.id,
         blocks: [
-          B.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
+          markdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
       });
       logger.error('Failed to create page in GROWI.');
       throw err;