hackmd.js 5.3 KB

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