togetter.js 7.2 KB

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