hackmd.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const logger = require('@alias/logger')('growi:routes:hackmd');
  2. const path = require('path');
  3. const fs = require('graceful-fs');
  4. const swig = require('swig-templates');
  5. const axios = require('axios');
  6. const ApiResponse = require('../util/apiResponse');
  7. module.exports = function(crowi, app) {
  8. const config = crowi.getConfig();
  9. const Page = crowi.models.Page;
  10. const pageEvent = crowi.event('page');
  11. // load GROWI agent script for HackMD
  12. const manifest = require(path.join(crowi.publicDir, 'manifest.json'));
  13. const agentScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-agent.js']);
  14. const stylesScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-styles.js']);
  15. // generate swig template
  16. let agentScriptContentTpl = undefined;
  17. let stylesScriptContentTpl = undefined;
  18. // init 'saveOnHackmd' event
  19. pageEvent.on('saveOnHackmd', function(page) {
  20. crowi.getIo().sockets.emit('page:editingWithHackmd', {page});
  21. });
  22. /**
  23. * GET /_hackmd/load-agent
  24. *
  25. * loadAgent action
  26. * This should be access from HackMD and send agent script
  27. *
  28. * @param {object} req
  29. * @param {object} res
  30. */
  31. const loadAgent = function(req, res) {
  32. // generate swig template
  33. if (agentScriptContentTpl == null) {
  34. agentScriptContentTpl = swig.compileFile(agentScriptPath);
  35. }
  36. let origin = `${req.protocol}://${req.get('host')}`;
  37. // use config.crowi['app:url'] when exist req.headers['x-forwarded-proto'].
  38. // refs: lib/crowi/express-init.js
  39. if (config.crowi && config.crowi['app:url']) {
  40. origin = config.crowi['app:url'];
  41. }
  42. // generate definitions to replace
  43. const definitions = {
  44. origin,
  45. };
  46. // inject
  47. const script = agentScriptContentTpl(definitions);
  48. res.set('Content-Type', 'application/javascript');
  49. res.send(script);
  50. };
  51. /**
  52. * GET /_hackmd/load-styles
  53. *
  54. * loadStyles action
  55. * This should be access from HackMD and send script to insert styles
  56. *
  57. * @param {object} req
  58. * @param {object} res
  59. */
  60. const loadStyles = function(req, res) {
  61. // generate swig template
  62. if (stylesScriptContentTpl == null) {
  63. stylesScriptContentTpl = swig.compileFile(stylesScriptPath);
  64. }
  65. const styleFilePath = path.join(crowi.publicDir, manifest['styles/style-hackmd.css']);
  66. const styles = fs.readFileSync(styleFilePath).toString().replace(/\s+/g, ' ');
  67. // generate definitions to replace
  68. const definitions = {
  69. styles,
  70. };
  71. // inject
  72. const script = stylesScriptContentTpl(definitions);
  73. res.set('Content-Type', 'application/javascript');
  74. res.send(script);
  75. };
  76. const validateForApi = async function(req, res, next) {
  77. // validate process.env.HACKMD_URI
  78. const hackmdUri = process.env.HACKMD_URI;
  79. if (hackmdUri == null) {
  80. return res.json(ApiResponse.error('HackMD for GROWI has not been setup'));
  81. }
  82. // validate pageId
  83. const pageId = req.body.pageId;
  84. if (pageId == null) {
  85. return res.json(ApiResponse.error('pageId required'));
  86. }
  87. // validate page
  88. const page = await Page.findOne({ _id: pageId });
  89. if (page == null) {
  90. return res.json(ApiResponse.error(`Page('${pageId}') does not exist`));
  91. }
  92. req.page = page;
  93. next();
  94. };
  95. /**
  96. * POST /_api/hackmd.integrate
  97. *
  98. * Create page on HackMD and start to integrate
  99. * @param {object} req
  100. * @param {object} res
  101. */
  102. const integrate = async function(req, res) {
  103. const hackmdUri = process.env.HACKMD_URI_FOR_SERVER || process.env.HACKMD_URI;
  104. let page = req.page;
  105. if (page.pageIdOnHackmd != null) {
  106. try {
  107. // check if page exists in HackMD
  108. await axios.get(`${hackmdUri}/${page.pageIdOnHackmd}`);
  109. }
  110. catch (err) {
  111. // reset if pages doesn't exist
  112. page.pageIdOnHackmd = undefined;
  113. }
  114. }
  115. try {
  116. if (page.pageIdOnHackmd == null) {
  117. page = await createNewPageOnHackmdAndRegister(hackmdUri, page);
  118. }
  119. else {
  120. page = await Page.syncRevisionToHackmd(page);
  121. }
  122. const data = {
  123. pageIdOnHackmd: page.pageIdOnHackmd,
  124. revisionIdHackmdSynced: page.revisionHackmdSynced,
  125. hasDraftOnHackmd: page.hasDraftOnHackmd,
  126. };
  127. return res.json(ApiResponse.success(data));
  128. }
  129. catch (err) {
  130. logger.error(err);
  131. return res.json(ApiResponse.error('Integration with HackMD process failed'));
  132. }
  133. };
  134. async function createNewPageOnHackmdAndRegister(hackmdUri, page) {
  135. // access to HackMD and create page
  136. const response = await axios.get(`${hackmdUri}/new`);
  137. logger.debug('HackMD responds', response);
  138. // extract page id on HackMD
  139. const pagePathOnHackmd = response.request.path; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
  140. const pageIdOnHackmd = pagePathOnHackmd.substr(1); // strip the head '/'
  141. return Page.registerHackmdPage(page, pageIdOnHackmd);
  142. }
  143. /**
  144. * POST /_api/hackmd.saveOnHackmd
  145. *
  146. * receive when save operation triggered on HackMD
  147. * !! This will be invoked many time from many people !!
  148. *
  149. * @param {object} req
  150. * @param {object} res
  151. */
  152. const saveOnHackmd = async function(req, res) {
  153. const page = req.page;
  154. try {
  155. await Page.updateHasDraftOnHackmd(page, true);
  156. pageEvent.emit('saveOnHackmd', page);
  157. return res.json(ApiResponse.success());
  158. }
  159. catch (err) {
  160. logger.error(err);
  161. return res.json(ApiResponse.error('saveOnHackmd process failed'));
  162. }
  163. };
  164. return {
  165. loadAgent,
  166. loadStyles,
  167. validateForApi,
  168. integrate,
  169. saveOnHackmd,
  170. };
  171. };