| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- const logger = require('@alias/logger')('growi:service:SlackBotService');
- const mongoose = require('mongoose');
- const axios = require('axios');
- const { markdownSectionBlock } = 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');
- class SlackBotService extends S2sMessageHandlable {
- constructor(crowi) {
- super();
- this.crowi = crowi;
- this.s2sMessagingService = crowi.s2sMessagingService;
- this.lastLoadedAt = null;
- this.initialize();
- }
- initialize() {
- this.lastLoadedAt = new Date();
- }
- /**
- * @inheritdoc
- */
- shouldHandleS2sMessage(s2sMessage) {
- const { eventName, updatedAt } = s2sMessage;
- if (eventName !== 'slackBotServiceUpdated' || updatedAt == null) {
- return false;
- }
- return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
- }
- /**
- * @inheritdoc
- */
- async handleS2sMessage() {
- const { configManager } = this.crowi;
- logger.info('Reset slack bot by pubsub notification');
- await configManager.loadConfigs();
- this.initialize();
- }
- async publishUpdatedMessage() {
- const { s2sMessagingService } = this;
- if (s2sMessagingService != null) {
- const s2sMessage = new S2sMessage('slackBotServiceUpdated', { updatedAt: new Date() });
- try {
- await s2sMessagingService.publish(s2sMessage);
- }
- catch (e) {
- logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
- }
- }
- }
- /**
- * 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({
- channel: body.channel_id,
- user: body.user_id,
- text: 'No command',
- blocks: [
- markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
- ],
- });
- return;
- }
- generatePageLinkMrkdwn(pathname, href) {
- return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
- }
- appendSpeechBaloon(mrkdwn, commentCount) {
- return (commentCount != null && commentCount > 0)
- ? `${mrkdwn} :speech_balloon: ${commentCount}`
- : mrkdwn;
- }
- generateLastUpdateMrkdwn(updatedAt, baseDate) {
- if (updatedAt != null) {
- // cast to date
- const date = new Date(updatedAt);
- return formatDistanceStrict(date, baseDate);
- }
- return '';
- }
- async shareSinglePage(client, payload) {
- const { channel, user, actions } = payload;
- const appUrl = this.crowi.appService.getSiteUrl();
- const appTitle = this.crowi.appService.getAppTitle();
- const channelId = channel.id;
- const action = actions[0]; // shareSinglePage action must have button action
- // restore page data from value
- const { page, href, pathname } = JSON.parse(action.value);
- const { updatedAt, commentCount } = page;
- // share
- const now = new Date();
- return client.chat.postMessage({
- channel: channelId,
- blocks: [
- { type: 'divider' },
- markdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
- {
- type: 'context',
- elements: [
- {
- type: 'mrkdwn',
- text: `<${decodeURI(appUrl)}|*${appTitle}*> | Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)} | Shared by *${user.username}*`,
- },
- ],
- },
- ],
- });
- }
- async dismissSearchResults(client, payload) {
- const { response_url: responseUrl } = payload;
- return axios.post(responseUrl, {
- delete_original: true,
- });
- }
- // Submit action in create Modal
- async createPage(client, payload, path, channelId, contentsBody) {
- const Page = this.crowi.model('Page');
- const pathUtils = require('growi-commons').pathUtils;
- const reshapedContentsBody = reshapeContentsBody(contentsBody);
- try {
- // sanitize path
- const sanitizedPath = this.crowi.xss.process(path);
- const normalizedPath = pathUtils.normalizePath(sanitizedPath);
- // generate a dummy id because Operation to create a page needs ObjectId
- const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
- const page = await Page.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
- // Send a message when page creation is complete
- const growiUri = this.crowi.appService.getSiteUrl();
- await client.chat.postEphemeral({
- channel: channelId,
- user: payload.user.id,
- text: `The page <${decodeURI(`${growiUri}/${page._id} | ${decodeURI(growiUri + normalizedPath)}`)}> has been created.`,
- });
- }
- catch (err) {
- client.chat.postMessage({
- channel: payload.user.id,
- blocks: [
- markdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${reshapedContentsBody}`)],
- });
- logger.error('Failed to create page in GROWI.');
- throw err;
- }
- }
- async createPageInGrowi(client, payload) {
- const path = payload.view.state.values.path.path_input.value;
- const channelId = JSON.parse(payload.view.private_metadata).channelId;
- const contentsBody = payload.view.state.values.contents.contents_input.value;
- await this.createPage(client, payload, path, channelId, contentsBody);
- }
- async togetterCreatePageInGrowi(client, payload) {
- const { response_url: responseUrl } = payload;
- const selectedOptions = payload.state.values.selected_messages.checkboxes_changed.selected_options;
- const messages = selectedOptions.map((option) => {
- const header = option.text.text.concat('\n');
- const body = option.description.text.concat('\n');
- return header.concat(body);
- });
- let path = '';
- let channelId = '';
- if (payload.type === 'block_actions' && payload.actions[0].action_id === 'togetterCreatePage') {
- path = payload.state.values.page_path.page_path.value;
- channelId = payload.channel.id;
- }
- const contentsBody = messages.join('');
- // dismiss
- axios.post(responseUrl, {
- delete_original: true,
- });
- await this.createPage(client, payload, path, channelId, contentsBody);
- }
- }
- module.exports = SlackBotService;
|