2
0

hackmd.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import * as hackmdFiles from '@growi/hackmd';
  2. import loggerFactory from '~/utils/logger';
  3. /* eslint-disable no-use-before-define */
  4. const logger = loggerFactory('growi:routes:hackmd');
  5. const axios = require('axios');
  6. const ejs = require('ejs');
  7. const ApiResponse = require('../util/apiResponse');
  8. /**
  9. * @swagger
  10. *
  11. * components:
  12. * schemas:
  13. * Hackmd:
  14. * description: Hackmd
  15. * type: object
  16. * properties:
  17. * pageIdOnHackmd:
  18. * type: string
  19. * description: page ID on HackMD
  20. * example: qLnodHLxT6C3hVEVczvbDQ
  21. * revisionIdHackmdSynced:
  22. * $ref: '#/components/schemas/Revision/properties/_id'
  23. * hasDraftOnHackmd:
  24. * type: boolean
  25. * description: has draft on HackMD
  26. * example: false
  27. */
  28. module.exports = function(crowi, app) {
  29. const Page = crowi.models.Page;
  30. const pageEvent = crowi.event('page');
  31. /**
  32. * GET /_hackmd/load-agent
  33. *
  34. * loadAgent action
  35. * This should be access from HackMD and send agent script
  36. *
  37. * @param {object} req
  38. * @param {object} res
  39. */
  40. const loadAgent = function(req, res) {
  41. const origin = crowi.appService.getSiteUrl();
  42. // generate definitions to replace
  43. const definitions = {
  44. origin,
  45. };
  46. // inject origin to script
  47. const script = ejs.render(hackmdFiles.agentJS, 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 definitions to replace
  62. const definitions = {
  63. styles: hackmdFiles.stylesCSS,
  64. };
  65. // inject styles to script
  66. const script = ejs.render(hackmdFiles.stylesJS, definitions);
  67. res.set('Content-Type', 'application/javascript');
  68. res.send(script);
  69. };
  70. const validateForApi = async function(req, res, next) {
  71. // validate process.env.HACKMD_URI
  72. const hackmdUri = process.env.HACKMD_URI;
  73. if (hackmdUri == null) {
  74. return res.json(ApiResponse.error('HackMD for GROWI has not been setup'));
  75. }
  76. // validate pageId
  77. const pageId = req.body.pageId;
  78. if (pageId == null) {
  79. return res.json(ApiResponse.error('pageId required'));
  80. }
  81. // validate page
  82. const page = await Page.findOne({ _id: pageId });
  83. if (page == null) {
  84. return res.json(ApiResponse.error(`Page('${pageId}') does not exist`));
  85. }
  86. req.page = page;
  87. next();
  88. };
  89. /**
  90. * @swagger
  91. *
  92. * /hackmd.integrate:
  93. * post:
  94. * tags: [Hackmd]
  95. * operationId: integrateHackmd
  96. * summary: /hackmd.integrate
  97. * description: Integrate hackmd
  98. * requestBody:
  99. * content:
  100. * application/json:
  101. * schema:
  102. * properties:
  103. * pageId:
  104. * $ref: '#/components/schemas/Page/properties/_id'
  105. * page:
  106. * $ref: '#/components/schemas/Hackmd'
  107. * responses:
  108. * 200:
  109. * description: Succeeded to integrate HackMD.
  110. * content:
  111. * application/json:
  112. * schema:
  113. * properties:
  114. * ok:
  115. * $ref: '#/components/schemas/V1Response/properties/ok'
  116. * pageIdOnHackmd:
  117. * $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
  118. * revisionIdHackmdSynced:
  119. * $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
  120. * hasDraftOnHackmd:
  121. * $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
  122. * 403:
  123. * $ref: '#/components/responses/403'
  124. * 500:
  125. * $ref: '#/components/responses/500'
  126. */
  127. /**
  128. * POST /_api/hackmd.integrate
  129. *
  130. * Create page on HackMD and start to integrate
  131. * @param {object} req
  132. * @param {object} res
  133. */
  134. const integrate = async function(req, res) {
  135. const hackmdUri = process.env.HACKMD_URI_FOR_SERVER || process.env.HACKMD_URI;
  136. let page = req.page;
  137. const hackmdPageUri = (page.pageIdOnHackmd != null)
  138. ? `${hackmdUri}/${page.pageIdOnHackmd}`
  139. : `${hackmdUri}/new`;
  140. let hackmdResponse;
  141. try {
  142. // check if page is found or created in HackMD
  143. hackmdResponse = await axios.get(hackmdPageUri, {
  144. maxRedirects: 0,
  145. // validate HTTP status is 200 or 302 or 404
  146. validateStatus: (status) => {
  147. return status === 200 || status === 302 || status === 404;
  148. },
  149. });
  150. }
  151. catch (err) {
  152. logger.error(err);
  153. return res.json(ApiResponse.error(err));
  154. }
  155. const { status, headers } = hackmdResponse;
  156. // validate HackMD/CodiMD/HedgeDoc specific header
  157. if (headers['codimd-version'] == null && headers['hackmd-version'] == null && headers['hedgedoc-version'] == null) {
  158. const message = 'Connecting to a non-HackMD server.';
  159. logger.error(message);
  160. return res.json(ApiResponse.error(message));
  161. }
  162. try {
  163. // when page is not found
  164. if (status === 404) {
  165. // reset registered data
  166. page = await Page.registerHackmdPage(page, undefined);
  167. // re-invoke
  168. return integrate(req, res);
  169. }
  170. // when redirect
  171. if (status === 302) {
  172. // extract page id on HackMD
  173. const pathnameOnHackmd = new URL(headers.location, hackmdUri).pathname; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
  174. const pageIdOnHackmd = pathnameOnHackmd.substr(1); // strip the head '/'
  175. page = await Page.registerHackmdPage(page, pageIdOnHackmd);
  176. }
  177. // when page is found
  178. else {
  179. page = await Page.syncRevisionToHackmd(page);
  180. }
  181. const data = {
  182. pageIdOnHackmd: page.pageIdOnHackmd,
  183. revisionIdHackmdSynced: page.revisionHackmdSynced,
  184. hasDraftOnHackmd: page.hasDraftOnHackmd,
  185. };
  186. return res.json(ApiResponse.success(data));
  187. }
  188. catch (err) {
  189. logger.error(err);
  190. return res.json(ApiResponse.error('Integration with HackMD process failed'));
  191. }
  192. };
  193. /**
  194. * @swagger
  195. *
  196. * /hackmd.discard:
  197. * post:
  198. * tags: [Hackmd]
  199. * operationId: discardHackmd
  200. * summary: /hackmd.discard
  201. * description: Discard hackmd
  202. * requestBody:
  203. * content:
  204. * application/json:
  205. * schema:
  206. * properties:
  207. * pageId:
  208. * $ref: '#/components/schemas/Page/properties/_id'
  209. * page:
  210. * $ref: '#/components/schemas/Hackmd'
  211. * responses:
  212. * 200:
  213. * description: Succeeded to integrate HackMD.
  214. * content:
  215. * application/json:
  216. * schema:
  217. * properties:
  218. * ok:
  219. * $ref: '#/components/schemas/V1Response/properties/ok'
  220. * pageIdOnHackmd:
  221. * $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
  222. * revisionIdHackmdSynced:
  223. * $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
  224. * hasDraftOnHackmd:
  225. * $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
  226. * 403:
  227. * $ref: '#/components/responses/403'
  228. * 500:
  229. * $ref: '#/components/responses/500'
  230. */
  231. /**
  232. * POST /_api/hackmd.discard
  233. *
  234. * Create page on HackMD and start to integrate
  235. * @param {object} req
  236. * @param {object} res
  237. */
  238. const discard = async function(req, res) {
  239. let page = req.page;
  240. try {
  241. page = await Page.syncRevisionToHackmd(page);
  242. const data = {
  243. pageIdOnHackmd: page.pageIdOnHackmd,
  244. revisionIdHackmdSynced: page.revisionHackmdSynced,
  245. hasDraftOnHackmd: page.hasDraftOnHackmd,
  246. };
  247. return res.json(ApiResponse.success(data));
  248. }
  249. catch (err) {
  250. logger.error(err);
  251. return res.json(ApiResponse.error('discard process failed'));
  252. }
  253. };
  254. /**
  255. * @swagger
  256. *
  257. * /hackmd.saveOnHackmd:
  258. * post:
  259. * tags: [Hackmd]
  260. * operationId: saveOnHackmd
  261. * summary: /hackmd.saveOnHackmd
  262. * description: Receive when save operation triggered on HackMD
  263. * requestBody:
  264. * content:
  265. * application/json:
  266. * schema:
  267. * properties:
  268. * pageId:
  269. * $ref: '#/components/schemas/Page/properties/_id'
  270. * page:
  271. * $ref: '#/components/schemas/Hackmd'
  272. * responses:
  273. * 200:
  274. * description: Succeeded to receive when save operation triggered on HackMD.
  275. * content:
  276. * application/json:
  277. * schema:
  278. * properties:
  279. * ok:
  280. * $ref: '#/components/schemas/V1Response/properties/ok'
  281. * 403:
  282. * $ref: '#/components/responses/403'
  283. * 500:
  284. * $ref: '#/components/responses/500'
  285. */
  286. /**
  287. * POST /_api/hackmd.saveOnHackmd
  288. *
  289. * receive when save operation triggered on HackMD
  290. * !! This will be invoked many time from many people !!
  291. *
  292. * @param {object} req
  293. * @param {object} res
  294. */
  295. const saveOnHackmd = async function(req, res) {
  296. const { page, user } = req;
  297. try {
  298. await Page.updateHasDraftOnHackmd(page, true);
  299. pageEvent.emit('saveOnHackmd', page, user);
  300. return res.json(ApiResponse.success());
  301. }
  302. catch (err) {
  303. logger.error(err);
  304. return res.json(ApiResponse.error('saveOnHackmd process failed'));
  305. }
  306. };
  307. return {
  308. loadAgent,
  309. loadStyles,
  310. validateForApi,
  311. integrate,
  312. discard,
  313. saveOnHackmd,
  314. };
  315. };