bolt.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. const logger = require('@alias/logger')('growi:service:BoltService');
  2. class BoltReciever {
  3. init(app) {
  4. this.bolt = app;
  5. }
  6. async requestHandler(req, res) {
  7. let ackCalled = false;
  8. // for verification request URL on Event Subscriptions
  9. if (req.body.challenge && req.body.type) {
  10. return res.send(req.body);
  11. }
  12. const payload = req.body.payload;
  13. let reqBody;
  14. if (payload != null) {
  15. reqBody = JSON.parse(payload);
  16. }
  17. else {
  18. reqBody = req.body;
  19. }
  20. const event = {
  21. body: reqBody,
  22. ack: (response) => {
  23. if (ackCalled) {
  24. return;
  25. }
  26. if (response instanceof Error) {
  27. res.status(500).send();
  28. }
  29. else if (!response) {
  30. res.send('');
  31. }
  32. else {
  33. res.send(response);
  34. }
  35. ackCalled = true;
  36. },
  37. };
  38. await this.bolt.processEvent(event);
  39. }
  40. }
  41. const { App } = require('@slack/bolt');
  42. const { WebClient, LogLevel } = require('@slack/web-api');
  43. class BoltService {
  44. constructor(crowi) {
  45. this.crowi = crowi;
  46. this.receiver = new BoltReciever();
  47. const signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
  48. const token = crowi.configManager.getConfig('crowi', 'slackbot:token');
  49. const client = new WebClient(token, { logLevel: LogLevel.DEBUG });
  50. this.client = client;
  51. if (token != null || signingSecret != null) {
  52. logger.debug('TwitterStrategy: setup is done');
  53. this.bolt = new App({
  54. token,
  55. signingSecret,
  56. receiver: this.receiver,
  57. });
  58. this.init();
  59. }
  60. }
  61. init() {
  62. // Example of listening for event
  63. // See. https://github.com/slackapi/bolt-js#listening-for-events
  64. // or https://slack.dev/bolt-js/concepts#basic
  65. this.bolt.command('/growi-bot', async({ command, ack, say }) => { // demo
  66. await say('Hello');
  67. });
  68. // The echo command simply echoes on command
  69. this.bolt.command('/echo', async({ command, ack, say }) => {
  70. // Acknowledge command request
  71. await ack();
  72. await say(`${command.text}`);
  73. });
  74. this.bolt.command('/growi', async({
  75. command, client, body, ack,
  76. }) => {
  77. await ack();
  78. const args = command.text.split(' ');
  79. const firstArg = args[0];
  80. switch (firstArg) {
  81. case 'search':
  82. this.searchResults(command, args);
  83. break;
  84. case 'create':
  85. this.createModal(command, client, body);
  86. break;
  87. default:
  88. this.notCommand(command);
  89. break;
  90. }
  91. });
  92. this.bolt.view('createPage', async({ ack, view }) => {
  93. await ack();
  94. return this.createPageInGrowi(view);
  95. });
  96. this.bolt.action('button_click', async({ body, ack, say }) => {
  97. await ack();
  98. await say('clicked the button');
  99. });
  100. }
  101. notCommand(command) {
  102. logger.error('Input first arguments');
  103. return this.client.chat.postEphemeral({
  104. channel: command.channel_id,
  105. user: command.user_id,
  106. blocks: [
  107. this.generateMarkdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
  108. ],
  109. });
  110. }
  111. async searchResults(command, args) {
  112. const firstKeyword = args[1];
  113. if (firstKeyword == null) {
  114. return this.client.chat.postEphemeral({
  115. channel: command.channel_id,
  116. user: command.user_id,
  117. blocks: [
  118. this.generateMarkdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
  119. ],
  120. });
  121. }
  122. // remove leading 'search'.
  123. args.shift();
  124. const keywords = args.join(' ');
  125. const { searchService } = this.crowi;
  126. const option = { limit: 10 };
  127. const results = await searchService.searchKeyword(keywords, null, {}, option);
  128. // no search results
  129. if (results.data.length === 0) {
  130. return this.client.chat.postEphemeral({
  131. channel: command.channel_id,
  132. user: command.user_id,
  133. blocks: [
  134. this.generateMarkdownSectionBlock('*No page that match your keywords.*'),
  135. ],
  136. });
  137. }
  138. const resultPaths = results.data.map((data) => {
  139. return data._source.path;
  140. });
  141. try {
  142. await this.client.chat.postEphemeral({
  143. channel: command.channel_id,
  144. user: command.user_id,
  145. blocks: [
  146. this.generateMarkdownSectionBlock('10 results.'),
  147. this.generateMarkdownSectionBlock(`${resultPaths.join('\n')}`),
  148. {
  149. type: 'actions',
  150. elements: [
  151. {
  152. type: 'button',
  153. text: {
  154. type: 'plain_text',
  155. text: 'Share the results in this channel.',
  156. },
  157. style: 'primary',
  158. action_id: 'button_click',
  159. },
  160. ],
  161. },
  162. ],
  163. });
  164. }
  165. catch {
  166. logger.error('Failed to get search results.');
  167. await this.client.chat.postEphemeral({
  168. channel: command.channel_id,
  169. user: command.user_id,
  170. blocks: [
  171. this.generateMarkdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
  172. ],
  173. });
  174. }
  175. }
  176. async createModal(command, client, body) {
  177. const User = this.crowi.model('User');
  178. try {
  179. const slackUser = await User.findUserByUsername('slackUser');
  180. // if "slackUser" is null, don't show create Modal
  181. if (slackUser == null) {
  182. throw new Error('userNull');
  183. }
  184. await client.views.open({
  185. trigger_id: body.trigger_id,
  186. view: {
  187. type: 'modal',
  188. callback_id: 'createPage',
  189. title: {
  190. type: 'plain_text',
  191. text: 'Create Page',
  192. },
  193. submit: {
  194. type: 'plain_text',
  195. text: 'Submit',
  196. },
  197. close: {
  198. type: 'plain_text',
  199. text: 'Cancel',
  200. },
  201. blocks: [
  202. this.generateMarkdownSectionBlock('Create new page.'),
  203. this.generateInputSectionBlock('path', 'Path', 'path_input', false, '/path'),
  204. this.generateInputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
  205. ],
  206. },
  207. });
  208. }
  209. catch (e) {
  210. if (e instanceof Error) {
  211. return this.client.chat.postEphemeral({
  212. channel: command.channel_id,
  213. user: command.user_id,
  214. blocks: [
  215. this.generateMarkdownSectionBlock('*slackUser does not exist.*'),
  216. ],
  217. });
  218. }
  219. logger.error('Failed to create page.');
  220. await this.client.chat.postEphemeral({
  221. channel: command.channel_id,
  222. user: command.user_id,
  223. blocks: [
  224. this.generateMarkdownSectionBlock('*Failed to create new page.*\n Hint\n `/growi create`'),
  225. ],
  226. });
  227. }
  228. }
  229. // Submit action in create Modal
  230. async createPageInGrowi(view) {
  231. const User = this.crowi.model('User');
  232. const Page = this.crowi.model('Page');
  233. const pathUtils = require('growi-commons').pathUtils;
  234. try {
  235. // search "slackUser" to create page in slack
  236. const slackUser = await User.findUserByUsername('slackUser');
  237. let path = view.state.values.path.path_input.value;
  238. const body = view.state.values.contents.contents_input.value;
  239. // sanitize path
  240. path = this.crowi.xss.process(path);
  241. path = pathUtils.normalizePath(path);
  242. const user = slackUser._id;
  243. return Page.create(path, body, user, {});
  244. }
  245. catch {
  246. logger.error('Failed to create page in GROWI.');
  247. }
  248. }
  249. generateMarkdownSectionBlock(blocks) {
  250. return {
  251. type: 'section',
  252. text: {
  253. type: 'mrkdwn',
  254. text: blocks,
  255. },
  256. };
  257. }
  258. generateInputSectionBlock(blockId, labelText, actionId, isMultiline, placeholder) {
  259. return {
  260. type: 'input',
  261. block_id: blockId,
  262. label: {
  263. type: 'plain_text',
  264. text: labelText,
  265. },
  266. element: {
  267. type: 'plain_text_input',
  268. action_id: actionId,
  269. multiline: isMultiline,
  270. placeholder: {
  271. type: 'plain_text',
  272. text: placeholder,
  273. },
  274. },
  275. };
  276. }
  277. }
  278. module.exports = BoltService;