Browse Source

Merge pull request #4037 from weseek/feat/GW-6710-slackbot-togetter-command-choose-growi

Feat/gw 6710 slackbot togetter command choose growi
Yuki Takei 4 years ago
parent
commit
5ca6ef287d

+ 1 - 1
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -26,7 +26,7 @@ import { SectionBlockPayloadDelegator } from '~/services/growi-uri-injector/Sect
 const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
 
 // temporarily save for selection to growi
-const temporarySinglePostCommands = ['create'];
+const temporarySinglePostCommands = ['create', 'togetter'];
 
 @Controller('/g2s')
 export class GrowiToSlackCtrl {

+ 220 - 0
src/server/service/slackbot.js

@@ -10,6 +10,8 @@ const { formatDistanceStrict, parse, format } = require('date-fns');
 const S2sMessage = require('../models/vo/s2s-message');
 const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
+const PAGINGLIMIT = 10;
+
 class SlackBotService extends S2sMessageHandlable {
 
   constructor(crowi) {
@@ -154,6 +156,224 @@ 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: [
+          markdownSectionBlock('*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 = [
+      markdownSectionBlock(`: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: [
+          markdownSectionBlock(err.toString()),
+        ],
+      });
+      throw new Error(err);
+    }
+  }
+
+  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: [
+          markdownSectionBlock('*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: [
+          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: [] };
+    }
+
+    const pages = results.data.map((data) => {
+      const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
+      return { path, updatedAt, commentCount };
+    });
+
+    return {
+      pages, offset, resultsTotal,
+    };
+  }
+
+  getKeywords(args) {
+    const keywordsArr = args.slice(1);
+    const keywords = keywordsArr.join(' ');
+    return keywords;
+  }
+
   // Submit action in create Modal
   async createPage(client, payload, path, channelId, contentsBody) {
     const Page = this.crowi.model('Page');