togetter.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import loggerFactory from '~/utils/logger';
  2. const logger = loggerFactory('growi:service:SlackBotService:togetter');
  3. const {
  4. inputBlock, actionsBlock, buttonElement, markdownSectionBlock, divider, respond,
  5. deleteOriginal,
  6. } = require('@growi/slack');
  7. const { parse, format } = require('date-fns');
  8. const axios = require('axios');
  9. const SlackbotError = require('../../models/vo/slackbot-error');
  10. module.exports = (crowi) => {
  11. const CreatePageService = require('./create-page-service');
  12. const createPageService = new CreatePageService(crowi);
  13. const BaseSlackCommandHandler = require('./slack-command-handler');
  14. const handler = new BaseSlackCommandHandler();
  15. handler.handleCommand = async function(growiCommand, client, body) {
  16. await respond(growiCommand.responseUrl, {
  17. text: 'Select messages to use.',
  18. blocks: this.togetterMessageBlocks(),
  19. });
  20. return;
  21. };
  22. handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName) {
  23. await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor);
  24. };
  25. handler.cancel = async function(client, payload, interactionPayloadAccessor) {
  26. await deleteOriginal(interactionPayloadAccessor.getResponseUrl(), {
  27. delete_original: true,
  28. });
  29. };
  30. handler.createPage = async function(client, payload, interactionPayloadAccessor) {
  31. let result = [];
  32. const channelId = payload.channel.id; // this must exist since the type is always block_actions
  33. const userChannelId = payload.user.id;
  34. try {
  35. // validate form
  36. const { path, oldest, newest } = await this.togetterValidateForm(client, payload, interactionPayloadAccessor);
  37. // get messages
  38. result = await this.togetterGetMessages(client, channelId, newest, oldest);
  39. // clean messages
  40. const cleanedContents = await this.togetterCleanMessages(result.messages);
  41. const contentsBody = cleanedContents.join('');
  42. // create and send url message
  43. await this.togetterCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userChannelId, contentsBody);
  44. }
  45. catch (err) {
  46. logger.error('Error occured by togetter.');
  47. throw err;
  48. }
  49. };
  50. handler.togetterValidateForm = async function(client, payload, interactionPayloadAccessor) {
  51. const grwTzoffset = crowi.appService.getTzoffset() * 60;
  52. const path = interactionPayloadAccessor.getStateValues()?.page_path.page_path.value;
  53. let oldest = interactionPayloadAccessor.getStateValues()?.oldest.oldest.value;
  54. let newest = interactionPayloadAccessor.getStateValues()?.newest.newest.value;
  55. oldest = oldest.trim();
  56. newest = newest.trim();
  57. if (path == null) {
  58. throw new SlackbotError({
  59. method: 'postMessage',
  60. to: 'dm',
  61. popupMessage: 'Page path is required.',
  62. mainMessage: 'Page path is required.',
  63. });
  64. }
  65. /**
  66. * RegExp for datetime yyyy/MM/dd-HH:mm
  67. * @see https://regex101.com/r/XbxdNo/1
  68. */
  69. const regexpDatetime = new RegExp(/^[12]\d\d\d\/(0[1-9]|1[012])\/(0[1-9]|[12][0-9]|3[01])-([01][0-9]|2[0123]):[0-5][0-9]$/);
  70. if (!regexpDatetime.test(oldest)) {
  71. throw new SlackbotError({
  72. method: 'postMessage',
  73. to: 'dm',
  74. popupMessage: 'Datetime format for oldest must be yyyy/MM/dd-HH:mm',
  75. mainMessage: 'Datetime format for oldest must be yyyy/MM/dd-HH:mm',
  76. });
  77. }
  78. if (!regexpDatetime.test(newest)) {
  79. throw new SlackbotError({
  80. method: 'postMessage',
  81. to: 'dm',
  82. popupMessage: 'Datetime format for newest must be yyyy/MM/dd-HH:mm',
  83. mainMessage: 'Datetime format for newest must be yyyy/MM/dd-HH:mm',
  84. });
  85. }
  86. oldest = parse(oldest, 'yyyy/MM/dd-HH:mm', new Date()).getTime() / 1000 + grwTzoffset;
  87. // + 60s in order to include messages between hh:mm.00s and hh:mm.59s
  88. newest = parse(newest, 'yyyy/MM/dd-HH:mm', new Date()).getTime() / 1000 + grwTzoffset + 60;
  89. if (oldest > newest) {
  90. throw new SlackbotError({
  91. method: 'postMessage',
  92. to: 'dm',
  93. popupMessage: 'Oldest datetime must be older than the newest date time.',
  94. mainMessage: 'Oldest datetime must be older than the newest date time.',
  95. });
  96. }
  97. return { path, oldest, newest };
  98. };
  99. handler.togetterGetMessages = async function(client, channelId, newest, oldest) {
  100. const result = await client.conversations.history({
  101. channel: channelId,
  102. newest,
  103. oldest,
  104. limit: 100,
  105. inclusive: true,
  106. });
  107. // return if no message found
  108. if (result.messages.length === 0) {
  109. throw new SlackbotError({
  110. method: 'postMessage',
  111. to: 'dm',
  112. popupMessage: 'No message found from togetter command. Try different datetime.',
  113. mainMessage: 'No message found from togetter command. Try different datetime.',
  114. });
  115. }
  116. return result;
  117. };
  118. handler.togetterCleanMessages = async function(messages) {
  119. const cleanedContents = [];
  120. let lastMessage = {};
  121. const grwTzoffset = crowi.appService.getTzoffset() * 60;
  122. messages
  123. .sort((a, b) => {
  124. return a.ts - b.ts;
  125. })
  126. .forEach((message) => {
  127. // increment contentsBody while removing the same headers
  128. // exclude header
  129. const lastMessageTs = Math.floor(lastMessage.ts / 60);
  130. const messageTs = Math.floor(message.ts / 60);
  131. if (lastMessage.user === message.user && lastMessageTs === messageTs) {
  132. cleanedContents.push(`${message.text}\n`);
  133. }
  134. // include header
  135. else {
  136. const ts = (parseInt(message.ts) - grwTzoffset) * 1000;
  137. const time = format(new Date(ts), 'h:mm a');
  138. cleanedContents.push(`${message.user} ${time}\n${message.text}\n`);
  139. lastMessage = message;
  140. }
  141. });
  142. return cleanedContents;
  143. };
  144. handler.togetterCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, userChannelId, contentsBody) {
  145. try {
  146. await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody);
  147. }
  148. catch (err) {
  149. logger.error('Error occurred while creating a page.');
  150. throw err;
  151. }
  152. try {
  153. // send preview to dm
  154. await client.chat.postMessage({
  155. channel: userChannelId,
  156. text: 'Preview from togetter command',
  157. blocks: [
  158. markdownSectionBlock('*Preview*'),
  159. divider(),
  160. markdownSectionBlock(contentsBody),
  161. divider(),
  162. ],
  163. });
  164. // dismiss
  165. await deleteOriginal(interactionPayloadAccessor.getResponseUrl(), {
  166. delete_original: true,
  167. });
  168. }
  169. catch (err) {
  170. logger.error('Error occurred while creating a page.', err);
  171. throw new SlackbotError({
  172. method: 'postMessage',
  173. to: 'dm',
  174. popupMessage: 'Error occurred while creating a page.',
  175. mainMessage: 'Error occurred while creating a page.',
  176. });
  177. }
  178. };
  179. handler.togetterMessageBlocks = function() {
  180. return [
  181. markdownSectionBlock('Select the oldest and newest datetime of the messages to use.'),
  182. inputBlock(this.plainTextInputElementWithInitialTime('oldest'), 'oldest', 'Oldest datetime'),
  183. inputBlock(this.plainTextInputElementWithInitialTime('newest'), 'newest', 'Newest datetime'),
  184. inputBlock(this.togetterInputBlockElement('page_path', '/'), 'page_path', 'Page path'),
  185. actionsBlock(
  186. buttonElement({ text: 'Cancel', actionId: 'togetter:cancel' }),
  187. buttonElement({ text: 'Create page', actionId: 'togetter:createPage', style: 'primary' }),
  188. ),
  189. ];
  190. };
  191. /**
  192. * Plain-text input element
  193. * https://api.slack.com/reference/block-kit/block-elements#input
  194. */
  195. handler.togetterInputBlockElement = function(actionId, placeholderText = 'Write something ...') {
  196. return {
  197. type: 'plain_text_input',
  198. placeholder: {
  199. type: 'plain_text',
  200. text: placeholderText,
  201. },
  202. action_id: actionId,
  203. };
  204. };
  205. handler.plainTextInputElementWithInitialTime = function(actionId) {
  206. const tzDateSec = new Date().getTime();
  207. const grwTzoffset = crowi.appService.getTzoffset() * 60 * 1000;
  208. const initialDateTime = format(new Date(tzDateSec - grwTzoffset), 'yyyy/MM/dd-HH:mm');
  209. return {
  210. type: 'plain_text_input',
  211. action_id: actionId,
  212. initial_value: initialDateTime,
  213. };
  214. };
  215. return handler;
  216. };