post.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. let marked = require('marked');
  2. let createDOMPurify = require('dompurify');
  3. let { JSDOM } = require('jsdom');
  4. let window = new JSDOM('').window;
  5. let DOMPurify = createDOMPurify(window);
  6. const Errors = require('../lib/errors')
  7. marked.setOptions({
  8. highlight: function (code) {
  9. return require('highlight.js').highlightAuto(code).value;
  10. }
  11. });
  12. const renderer = new marked.Renderer();
  13. renderer.link = function (href, title, text) {
  14. if(!href.match(/[a-z]+:\/\/.+/i)) {
  15. href = 'http://' + href;
  16. }
  17. return `
  18. <a href='${href}' ${title ? "title='" + title + "'" : "" } target='_blank' rel='noopener'>
  19. ${text}
  20. </a>
  21. `;
  22. };
  23. module.exports = (sequelize, DataTypes) => {
  24. let Post = sequelize.define('Post', {
  25. content: {
  26. type: DataTypes.TEXT,
  27. set (val) {
  28. if(!val) {
  29. throw Errors.sequelizeValidation(sequelize, {
  30. error: 'content must be a string',
  31. path: 'content'
  32. })
  33. }
  34. let rawHTML = marked(val, { renderer });
  35. let cleanHTML = DOMPurify.sanitize(rawHTML);
  36. let plainText = (new JSDOM(cleanHTML)).window.document.body.textContent;
  37. if (!plainText.trim().length) {
  38. throw Errors.sequelizeValidation(sequelize, {
  39. error: 'Post content must not be empty',
  40. path: 'content'
  41. })
  42. }
  43. this.setDataValue('content', cleanHTML)
  44. this.setDataValue('plainText', plainText)
  45. },
  46. allowNull: false
  47. },
  48. plainText: DataTypes.TEXT,
  49. postNumber: DataTypes.INTEGER,
  50. replyingToUsername: DataTypes.STRING,
  51. removed: {
  52. type: DataTypes.BOOLEAN,
  53. defaultValue: false
  54. }
  55. }, {
  56. instanceMethods: {
  57. getReplyingTo () {
  58. return Post.findByPrimary(this.replyId)
  59. },
  60. setReplyingTo (post) {
  61. return post.getUser().then(user => {
  62. return this.update({ replyingToUsername: user.username, replyId: post.id })
  63. })
  64. }
  65. },
  66. classMethods: {
  67. associate (models) {
  68. Post.belongsTo(models.User)
  69. Post.belongsTo(models.Thread)
  70. Post.hasMany(models.Post, { as: 'Replies', foreignKey: 'replyId' })
  71. Post.belongsToMany(models.User, { as: 'Likes', through: 'user_post' })
  72. Post.hasMany(models.Report, { foreignKeyConstraint: true, onDelete: 'CASCADE', hooks: true })
  73. },
  74. includeOptions () {
  75. let models = sequelize.models
  76. return [
  77. { model: models.User, attributes: ['username', 'createdAt', 'id', 'color', 'picture'] },
  78. { model: models.User, as: 'Likes', attributes: ['username', 'createdAt', 'id', 'color', 'picture'] },
  79. { model: models.Thread, include: [models.Category]} ,
  80. {
  81. model: models.Post, as: 'Replies', include:
  82. [{ model: models.User, attributes: ['username', 'id', 'color', 'picture'] }]
  83. }
  84. ]
  85. },
  86. async getReplyingToPost (id, thread) {
  87. let { Thread, User } = sequelize.models
  88. let replyingToPost = await Post.findById(
  89. id,
  90. { include: [Thread, { model: User, attributes: ['username'] }] }
  91. )
  92. if(!replyingToPost) {
  93. throw Errors.invalidParameter('replyingToId', 'post does not exist')
  94. } else if(replyingToPost.Thread.id !== thread.id) {
  95. throw Errors.invalidParameter('replyingToId', 'replies must be in same thread')
  96. } else if (replyingToPost.removed) {
  97. throw Errors.postRemoved
  98. } else {
  99. return replyingToPost
  100. }
  101. }
  102. }
  103. })
  104. return Post
  105. }