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