Add comments controller
authorChocobozzz <me@florianbigard.com>
Fri, 22 Dec 2017 09:50:07 +0000 (10:50 +0100)
committerChocobozzz <me@florianbigard.com>
Fri, 22 Dec 2017 10:29:12 +0000 (11:29 +0100)
server/controllers/api/videos/comment.ts
server/controllers/api/videos/index.ts
server/helpers/custom-validators/video-comments.ts [new file with mode: 0644]
server/initializers/constants.ts
server/lib/activitypub/url.ts
server/lib/video-comment.ts [new file with mode: 0644]
server/middlewares/sort.ts
server/middlewares/validators/sort.ts
server/middlewares/validators/video-comments.ts [new file with mode: 0644]
server/models/video/video-comment.ts
shared/models/videos/video-comment.model.ts [new file with mode: 0644]

index b69aa5d40f57d786bb3c9766a7fa22347e98f4a0..81c9e7d161b689d6ff4b064a427982bd716edef5 100644 (file)
-// import * as express from 'express'
-// import { logger, getFormattedObjects } from '../../../helpers'
-// import {
-//   authenticate,
-//   ensureUserHasRight,
-//   videosBlacklistAddValidator,
-//   videosBlacklistRemoveValidator,
-//   paginationValidator,
-//   blacklistSortValidator,
-//   setBlacklistSort,
-//   setPagination,
-//   asyncMiddleware
-// } from '../../../middlewares'
-// import { BlacklistedVideo, UserRight } from '../../../../shared'
-// import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
-//
-// const videoCommentRouter = express.Router()
-//
-// videoCommentRouter.get('/:videoId/comment',
-//   authenticate,
-//   ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
-//   asyncMiddleware(listVideoCommentsThreadsValidator),
-//   asyncMiddleware(listVideoCommentsThreads)
-// )
-//
-// videoCommentRouter.post('/:videoId/comment',
-//   authenticate,
-//   ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
-//   asyncMiddleware(videosBlacklistAddValidator),
-//   asyncMiddleware(addVideoToBlacklist)
-// )
-//
-// videoCommentRouter.get('/blacklist',
-//   authenticate,
-//   ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
-//   paginationValidator,
-//   blacklistSortValidator,
-//   setBlacklistSort,
-//   setPagination,
-//   asyncMiddleware(listBlacklist)
-// )
-//
-// videoCommentRouter.delete('/:videoId/blacklist',
-//   authenticate,
-//   ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
-//   asyncMiddleware(videosBlacklistRemoveValidator),
-//   asyncMiddleware(removeVideoFromBlacklistController)
-// )
-//
-// // ---------------------------------------------------------------------------
-//
-// export {
-//   videoCommentRouter
-// }
-//
-// // ---------------------------------------------------------------------------
-//
-// async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
-//   const videoInstance = res.locals.video
-//
-//   const toCreate = {
-//     videoId: videoInstance.id
-//   }
-//
-//   await VideoBlacklistModel.create(toCreate)
-//   return res.type('json').status(204).end()
-// }
-//
-// async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
-//   const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
-//
-//   return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total))
-// }
-//
-// async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
-//   const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel
-//
-//   try {
-//     await blacklistedVideo.destroy()
-//
-//     logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
-//
-//     return res.sendStatus(204)
-//   } catch (err) {
-//     logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, err)
-//     throw err
-//   }
-// }
+import * as express from 'express'
+import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
+import { getFormattedObjects, retryTransactionWrapper } from '../../../helpers'
+import { sequelizeTypescript } from '../../../initializers'
+import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment'
+import { asyncMiddleware, authenticate, paginationValidator, setPagination, setVideoCommentThreadsSort } from '../../../middlewares'
+import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
+import {
+  addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator,
+  listVideoThreadCommentsValidator
+} from '../../../middlewares/validators/video-comments'
+import { VideoCommentModel } from '../../../models/video/video-comment'
+
+const videoCommentRouter = express.Router()
+
+videoCommentRouter.get('/:videoId/comment-threads',
+  paginationValidator,
+  videoCommentThreadsSortValidator,
+  setVideoCommentThreadsSort,
+  setPagination,
+  asyncMiddleware(listVideoCommentThreadsValidator),
+  asyncMiddleware(listVideoThreads)
+)
+videoCommentRouter.get('/:videoId/comment-threads/:threadId',
+  asyncMiddleware(listVideoThreadCommentsValidator),
+  asyncMiddleware(listVideoThreadComments)
+)
+
+videoCommentRouter.post('/:videoId/comment-threads',
+  authenticate,
+  asyncMiddleware(addVideoCommentThreadValidator),
+  asyncMiddleware(addVideoCommentThreadRetryWrapper)
+)
+videoCommentRouter.post('/:videoId/comments/:commentId',
+  authenticate,
+  asyncMiddleware(addVideoCommentReplyValidator),
+  asyncMiddleware(addVideoCommentReplyRetryWrapper)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  videoCommentRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const resultList = await VideoCommentModel.listThreadsForApi(res.locals.video.id, req.query.start, req.query.count, req.query.sort)
+
+  return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
+
+async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const resultList = await VideoCommentModel.listThreadCommentsForApi(res.locals.video.id, res.locals.videoCommentThread.id)
+
+  return res.json(buildFormattedCommentTree(resultList))
+}
+
+async function addVideoCommentThreadRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot insert the video comment thread with many retries.'
+  }
+
+  const comment = await retryTransactionWrapper(addVideoCommentThread, options)
+
+  res.json({
+    comment: {
+      id: comment.id
+    }
+  }).end()
+}
+
+function addVideoCommentThread (req: express.Request, res: express.Response) {
+  const videoCommentInfo: VideoCommentCreate = req.body
+
+  return sequelizeTypescript.transaction(async t => {
+    return createVideoComment({
+      text: videoCommentInfo.text,
+      inReplyToComment: null,
+      video: res.locals.video,
+      actorId: res.locals.oauth.token.User.Account.Actor.id
+    }, t)
+  })
+}
+
+async function addVideoCommentReplyRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot insert the video comment reply with many retries.'
+  }
+
+  const comment = await retryTransactionWrapper(addVideoCommentReply, options)
+
+  res.json({
+    comment: {
+      id: comment.id
+    }
+  }).end()
+}
+
+function addVideoCommentReply (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const videoCommentInfo: VideoCommentCreate = req.body
+
+  return sequelizeTypescript.transaction(async t => {
+    return createVideoComment({
+      text: videoCommentInfo.text,
+      inReplyToComment: res.locals.videoComment.id,
+      video: res.locals.video,
+      actorId: res.locals.oauth.token.User.Account.Actor.id
+    }, t)
+  })
+}
index 8283f2e4e3a537b3eeea7c446f21be7f8fd8d8d4..8e54d95abd97d6ed4c3b72e0dc7c4fc511c58518 100644 (file)
@@ -47,6 +47,7 @@ import { VideoFileModel } from '../../../models/video/video-file'
 import { abuseVideoRouter } from './abuse'
 import { blacklistRouter } from './blacklist'
 import { videoChannelRouter } from './channel'
+import { videoCommentRouter } from './comment'
 import { rateVideoRouter } from './rate'
 
 const videosRouter = express.Router()
@@ -78,6 +79,7 @@ videosRouter.use('/', abuseVideoRouter)
 videosRouter.use('/', blacklistRouter)
 videosRouter.use('/', rateVideoRouter)
 videosRouter.use('/', videoChannelRouter)
+videosRouter.use('/', videoCommentRouter)
 
 videosRouter.get('/categories', listVideoCategories)
 videosRouter.get('/licences', listVideoLicences)
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
new file mode 100644 (file)
index 0000000..2b3f660
--- /dev/null
@@ -0,0 +1,16 @@
+import 'express-validator'
+import 'multer'
+import * as validator from 'validator'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
+
+const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
+
+function isValidVideoCommentText (value: string) {
+  return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isValidVideoCommentText
+}
index c8b21d10d3c3b67ee60960f15cea68090307a3ed..25b2dff8442efaa92b94f7f96196a4dc96ede7c2 100644 (file)
@@ -26,6 +26,7 @@ const SORTABLE_COLUMNS = {
   VIDEO_ABUSES: [ 'id', 'createdAt' ],
   VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
   VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
+  VIDEO_COMMENT_THREADS: [ 'createdAt' ],
   BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
   FOLLOWERS: [ 'createdAt' ],
   FOLLOWING: [ 'createdAt' ]
@@ -176,7 +177,8 @@ const CONSTRAINTS_FIELDS = {
   VIDEO_EVENTS: {
     COUNT: { min: 0 }
   },
-  COMMENT: {
+  VIDEO_COMMENTS: {
+    TEXT: { min: 2, max: 3000 }, // Length
     URL: { min: 3, max: 2000 } // Length
   }
 }
index bb2d4d11e7b4dfc6f15600c42365af9937f6a1da..729bb8dda446a8376a2a2be10e548eebdc83261e 100644 (file)
@@ -3,17 +3,18 @@ import { ActorModel } from '../../models/activitypub/actor'
 import { ActorFollowModel } from '../../models/activitypub/actor-follow'
 import { VideoModel } from '../../models/video/video'
 import { VideoAbuseModel } from '../../models/video/video-abuse'
+import { VideoCommentModel } from '../../models/video/video-comment'
 
 function getVideoActivityPubUrl (video: VideoModel) {
   return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
 }
 
-function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
-  return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID
+function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) {
+  return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '#comment-' + videoComment.id
 }
 
-function getApplicationActivityPubUrl () {
-  return CONFIG.WEBSERVER.URL + '/application/peertube'
+function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
+  return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID
 }
 
 function getAccountActivityPubUrl (accountName: string) {
@@ -63,7 +64,6 @@ function getUndoActivityPubUrl (originalUrl: string) {
 }
 
 export {
-  getApplicationActivityPubUrl,
   getVideoActivityPubUrl,
   getVideoChannelActivityPubUrl,
   getAccountActivityPubUrl,
@@ -75,5 +75,6 @@ export {
   getUndoActivityPubUrl,
   getVideoViewActivityPubUrl,
   getVideoLikeActivityPubUrl,
-  getVideoDislikeActivityPubUrl
+  getVideoDislikeActivityPubUrl,
+  getVideoCommentActivityPubUrl
 }
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
new file mode 100644 (file)
index 0000000..edb72d4
--- /dev/null
@@ -0,0 +1,74 @@
+import * as Sequelize from 'sequelize'
+import { ResultList } from '../../shared/models'
+import { VideoCommentThread } from '../../shared/models/videos/video-comment.model'
+import { VideoModel } from '../models/video/video'
+import { VideoCommentModel } from '../models/video/video-comment'
+import { getVideoCommentActivityPubUrl } from './activitypub'
+
+async function createVideoComment (obj: {
+  text: string,
+  inReplyToComment: number,
+  video: VideoModel
+  actorId: number
+}, t: Sequelize.Transaction) {
+  let originCommentId: number = null
+  if (obj.inReplyToComment) {
+    const repliedComment = await VideoCommentModel.loadById(obj.inReplyToComment)
+    if (!repliedComment) throw new Error('Unknown replied comment.')
+
+    originCommentId = repliedComment.originCommentId || repliedComment.id
+  }
+
+  const comment = await VideoCommentModel.create({
+    text: obj.text,
+    originCommentId,
+    inReplyToComment: obj.inReplyToComment,
+    videoId: obj.video.id,
+    actorId: obj.actorId
+  }, { transaction: t })
+
+  comment.set('url', getVideoCommentActivityPubUrl(obj.video, comment))
+
+  return comment.save({ transaction: t })
+}
+
+function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThread {
+  // Comments are sorted by id ASC
+  const comments = resultList.data
+
+  const comment = comments.shift()
+  const thread: VideoCommentThread = {
+    comment: comment.toFormattedJSON(),
+    children: []
+  }
+  const idx = {
+    [comment.id]: thread
+  }
+
+  while (comments.length !== 0) {
+    const childComment = comments.shift()
+
+    const childCommentThread: VideoCommentThread = {
+      comment: childComment.toFormattedJSON(),
+      children: []
+    }
+
+    const parentCommentThread = idx[childComment.inReplyToCommentId]
+    if (!parentCommentThread) {
+      const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}`
+      throw new Error(msg)
+    }
+
+    parentCommentThread.children.push(childCommentThread)
+    idx[childComment.id] = childCommentThread
+  }
+
+  return thread
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  createVideoComment,
+  buildFormattedCommentTree
+}
index 5d2a43accc019a8d83d860944936d4a6c82a7632..0eb50db893655dfd9c946e9ad1fa2699526d1957 100644 (file)
@@ -32,6 +32,12 @@ function setVideosSort (req: express.Request, res: express.Response, next: expre
   return next()
 }
 
+function setVideoCommentThreadsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+  if (!req.query.sort) req.query.sort = '-createdAt'
+
+  return next()
+}
+
 function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.query.sort) req.query.sort = '-createdAt'
 
@@ -75,5 +81,6 @@ export {
   setBlacklistSort,
   setFollowersSort,
   setFollowingSort,
-  setJobsSort
+  setJobsSort,
+  setVideoCommentThreadsSort
 }
index 38184fefa0dc6e6a0a98fd38c0c375aa6a70eb08..56855bda0e6bf17342df40439a9bb5a5a4ed86f1 100644 (file)
@@ -9,6 +9,7 @@ const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
 const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
 const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
 const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
+const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
 const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
 const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
 const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
@@ -18,6 +19,7 @@ const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
 const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
 const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
 const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
+const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
 const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
 const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
 const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
@@ -33,7 +35,8 @@ export {
   blacklistSortValidator,
   followersSortValidator,
   followingSortValidator,
-  jobsSortValidator
+  jobsSortValidator,
+  videoCommentThreadsSortValidator
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/video-comments.ts
new file mode 100644 (file)
index 0000000..5e1be00
--- /dev/null
@@ -0,0 +1,131 @@
+import * as express from 'express'
+import { body, param } from 'express-validator/check'
+import { logger } from '../../helpers'
+import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
+import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments'
+import { isVideoExist } from '../../helpers/custom-validators/videos'
+import { VideoCommentModel } from '../../models/video/video-comment'
+import { areValidationErrors } from './utils'
+
+const listVideoCommentThreadsValidator = [
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExist(req.params.videoId, res)) return
+
+    return next()
+  }
+]
+
+const listVideoThreadCommentsValidator = [
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExist(req.params.videoId, res)) return
+    if (!await isVideoCommentThreadExist(req.params.threadId, req.params.videoId, res)) return
+
+    return next()
+  }
+]
+
+const addVideoCommentThreadValidator = [
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExist(req.params.videoId, res)) return
+
+    return next()
+  }
+]
+
+const addVideoCommentReplyValidator = [
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
+  body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExist(req.params.videoId, res)) return
+    if (!await isVideoCommentExist(req.params.commentId, req.params.videoId, res)) return
+
+    return next()
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  listVideoCommentThreadsValidator,
+  listVideoThreadCommentsValidator,
+  addVideoCommentThreadValidator,
+  addVideoCommentReplyValidator
+}
+
+// ---------------------------------------------------------------------------
+
+async function isVideoCommentThreadExist (id: number, videoId: number, res: express.Response) {
+  const videoComment = await VideoCommentModel.loadById(id)
+
+  if (!videoComment) {
+    res.status(404)
+      .json({ error: 'Video comment thread not found' })
+      .end()
+
+    return false
+  }
+
+  if (videoComment.videoId !== videoId) {
+    res.status(400)
+      .json({ error: 'Video comment is associated to this video.' })
+      .end()
+
+    return false
+  }
+
+  if (videoComment.inReplyToCommentId !== null) {
+    res.status(400)
+      .json({ error: 'Video comment is not a thread.' })
+      .end()
+
+    return false
+  }
+
+  res.locals.videoCommentThread = videoComment
+  return true
+}
+
+async function isVideoCommentExist (id: number, videoId: number, res: express.Response) {
+  const videoComment = await VideoCommentModel.loadById(id)
+
+  if (!videoComment) {
+    res.status(404)
+      .json({ error: 'Video comment thread not found' })
+      .end()
+
+    return false
+  }
+
+  if (videoComment.videoId !== videoId) {
+    res.status(400)
+      .json({ error: 'Video comment is associated to this video.' })
+      .end()
+
+    return false
+  }
+
+  res.locals.videoComment = videoComment
+  return true
+}
index 92c0c6112fe08f37eb252ac600481261062370e2..d66f933ee714fd7ebffe7083882d6bf1f9aeea0b 100644 (file)
@@ -1,19 +1,34 @@
 import * as Sequelize from 'sequelize'
 import {
-  AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table,
+  AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { VideoComment } from '../../../shared/models/videos/video-comment.model'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
 import { CONSTRAINTS_FIELDS } from '../../initializers'
 import { ActorModel } from '../activitypub/actor'
-import { throwIfNotValid } from '../utils'
+import { getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 
+enum ScopeNames {
+  WITH_ACTOR = 'WITH_ACTOR'
+}
+
+@Scopes({
+  [ScopeNames.WITH_ACTOR]: {
+    include: [
+      () => ActorModel
+    ]
+  }
+})
 @Table({
   tableName: 'videoComment',
   indexes: [
     {
       fields: [ 'videoId' ]
+    },
+    {
+      fields: [ 'videoId', 'originCommentId' ]
     }
   ]
 })
@@ -81,6 +96,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
   })
   Actor: ActorModel
 
+  @AfterDestroy
+  static sendDeleteIfOwned (instance: VideoCommentModel) {
+    // TODO
+    return undefined
+  }
+
+  static loadById (id: number, t?: Sequelize.Transaction) {
+    const query: IFindOptions<VideoCommentModel> = {
+      where: {
+        id
+      }
+    }
+
+    if (t !== undefined) query.transaction = t
+
+    return VideoCommentModel.findOne(query)
+  }
+
   static loadByUrl (url: string, t?: Sequelize.Transaction) {
     const query: IFindOptions<VideoCommentModel> = {
       where: {
@@ -92,4 +125,55 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
 
     return VideoCommentModel.findOne(query)
   }
+
+  static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
+    const query = {
+      offset: start,
+      limit: count,
+      order: [ getSort(sort) ],
+      where: {
+        videoId
+      }
+    }
+
+    return VideoCommentModel
+      .scope([ ScopeNames.WITH_ACTOR ])
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
+  }
+
+  static listThreadCommentsForApi (videoId: number, threadId: number) {
+    const query = {
+      order: [ 'id', 'ASC' ],
+      where: {
+        videoId,
+        [ Sequelize.Op.or ]: [
+          { id: threadId },
+          { originCommentId: threadId }
+        ]
+      }
+    }
+
+    return VideoCommentModel
+      .scope([ ScopeNames.WITH_ACTOR ])
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
+  }
+
+  toFormattedJSON () {
+    return {
+      id: this.id,
+      url: this.url,
+      text: this.text,
+      threadId: this.originCommentId || this.id,
+      inReplyToCommentId: this.inReplyToCommentId,
+      videoId: this.videoId,
+      createdAt: this.createdAt,
+      updatedAt: this.updatedAt
+    } as VideoComment
+  }
 }
diff --git a/shared/models/videos/video-comment.model.ts b/shared/models/videos/video-comment.model.ts
new file mode 100644 (file)
index 0000000..bdeb30d
--- /dev/null
@@ -0,0 +1,19 @@
+export interface VideoComment {
+  id: number
+  url: string
+  text: string
+  threadId: number
+  inReplyToCommentId: number
+  videoId: number
+  createdAt: Date | string
+  updatedAt: Date | string
+}
+
+export interface VideoCommentThread {
+  comment: VideoComment
+  children: VideoCommentThread[]
+}
+
+export interface VideoCommentCreate {
+  text: string
+}