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; };