togetter.js 8.1 KB

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