hackmd.js 5.0 KB

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