return sequelizeTypescript.transaction(async t => {
return createVideoComment({
text: videoCommentInfo.text,
- inReplyToComment: null,
+ inReplyToCommentId: null,
video: res.locals.video,
- actorId: res.locals.oauth.token.User.Account.Actor.id
+ accountId: res.locals.oauth.token.User.Account.id
}, t)
})
}
return sequelizeTypescript.transaction(async t => {
return createVideoComment({
text: videoCommentInfo.text,
- inReplyToComment: res.locals.videoComment.id,
+ inReplyToCommentId: res.locals.videoComment.id,
video: res.locals.video,
- actorId: res.locals.oauth.token.User.Account.Actor.id
+ accountId: res.locals.oauth.token.User.Account.id
}, t)
})
}
originCommentId: null,
inReplyToComment: null,
videoId: video.id,
- actorId: byActor.id
+ accountId: byAccount.id
}, { transaction: t })
}
originCommentId,
inReplyToCommentId: inReplyToComment.id,
videoId: inReplyToComment.videoId,
- actorId: byActor.id
+ accountId: byAccount.id
}, { transaction: t })
})
}
import * as Sequelize from 'sequelize'
import { ResultList } from '../../shared/models'
-import { VideoCommentThread } from '../../shared/models/videos/video-comment.model'
+import { VideoCommentThreadTree } 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,
+ inReplyToCommentId: number,
video: VideoModel
- actorId: number
+ accountId: number
}, t: Sequelize.Transaction) {
let originCommentId: number = null
- if (obj.inReplyToComment) {
- const repliedComment = await VideoCommentModel.loadById(obj.inReplyToComment)
+
+ if (obj.inReplyToCommentId) {
+ const repliedComment = await VideoCommentModel.loadById(obj.inReplyToCommentId)
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,
+ inReplyToCommentId: obj.inReplyToCommentId,
videoId: obj.video.id,
- actorId: obj.actorId
- }, { transaction: t })
+ accountId: obj.accountId,
+ url: 'fake url'
+ }, { transaction: t, validate: false })
comment.set('url', getVideoCommentActivityPubUrl(obj.video, comment))
return comment.save({ transaction: t })
}
-function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThread {
+function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThreadTree {
// Comments are sorted by id ASC
const comments = resultList.data
const comment = comments.shift()
- const thread: VideoCommentThread = {
+ const thread: VideoCommentThreadTree = {
comment: comment.toFormattedJSON(),
children: []
}
while (comments.length !== 0) {
const childComment = comments.shift()
- const childCommentThread: VideoCommentThread = {
+ const childCommentThread: VideoCommentThreadTree = {
comment: childComment.toFormattedJSON(),
children: []
}
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments'
import { isVideoExist } from '../../helpers/custom-validators/videos'
+import { VideoModel } from '../../models/video/video'
import { VideoCommentModel } from '../../models/video/video-comment'
import { areValidationErrors } from './utils'
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 })
+ logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.videoId, res)) return
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 })
+ logger.debug('Checking listVideoThreadComments 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
+ if (!await isVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return
return next()
}
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 })
+ logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.videoId, res)) return
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 })
+ logger.debug('Checking addVideoCommentReply 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
+ if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return
return next()
}
// ---------------------------------------------------------------------------
-async function isVideoCommentThreadExist (id: number, videoId: number, res: express.Response) {
+async function isVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) {
const videoComment = await VideoCommentModel.loadById(id)
if (!videoComment) {
return false
}
- if (videoComment.videoId !== videoId) {
+ if (videoComment.videoId !== video.id) {
res.status(400)
.json({ error: 'Video comment is associated to this video.' })
.end()
return true
}
-async function isVideoCommentExist (id: number, videoId: number, res: express.Response) {
+async function isVideoCommentExist (id: number, video: VideoModel, res: express.Response) {
const videoComment = await VideoCommentModel.loadById(id)
if (!videoComment) {
return false
}
- if (videoComment.videoId !== videoId) {
+ if (videoComment.videoId !== video.id) {
res.status(400)
.json({ error: 'Video comment is associated to this video.' })
.end()
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 { AccountModel } from '../account/account'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
enum ScopeNames {
- WITH_ACTOR = 'WITH_ACTOR'
+ WITH_ACCOUNT = 'WITH_ACCOUNT'
}
@Scopes({
- [ScopeNames.WITH_ACTOR]: {
+ [ScopeNames.WITH_ACCOUNT]: {
include: [
- () => ActorModel
+ () => AccountModel
]
}
})
})
Video: VideoModel
- @ForeignKey(() => ActorModel)
+ @ForeignKey(() => AccountModel)
@Column
- actorId: number
+ accountId: number
- @BelongsTo(() => ActorModel, {
+ @BelongsTo(() => AccountModel, {
foreignKey: {
allowNull: false
},
onDelete: 'CASCADE'
})
- Actor: ActorModel
+ Account: AccountModel
@AfterDestroy
static sendDeleteIfOwned (instance: VideoCommentModel) {
limit: count,
order: [ getSort(sort) ],
where: {
- videoId
+ videoId,
+ inReplyToCommentId: null
}
}
return VideoCommentModel
- .scope([ ScopeNames.WITH_ACTOR ])
+ .scope([ ScopeNames.WITH_ACCOUNT ])
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
static listThreadCommentsForApi (videoId: number, threadId: number) {
const query = {
- order: [ 'id', 'ASC' ],
+ order: [ [ 'id', 'ASC' ] ],
where: {
videoId,
[ Sequelize.Op.or ]: [
}
return VideoCommentModel
- .scope([ ScopeNames.WITH_ACTOR ])
+ .scope([ ScopeNames.WITH_ACCOUNT ])
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
inReplyToCommentId: this.inReplyToCommentId,
videoId: this.videoId,
createdAt: this.createdAt,
- updatedAt: this.updatedAt
+ updatedAt: this.updatedAt,
+ account: {
+ name: this.Account.name
+ }
} as VideoComment
}
}
import './multiple-servers'
import './follows'
import './jobs'
+import './video-comments'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { VideoComment, VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
+import { dateIsValid, flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../utils'
+import { addVideoCommentReply, addVideoCommentThread, getVideoCommentThreads, getVideoThreadComments } from '../utils/video-comments'
+
+const expect = chai.expect
+
+describe('Test a video comments', function () {
+ let server: ServerInfo
+ let videoId
+ let videoUUID
+ let threadId
+
+ before(async function () {
+ this.timeout(10000)
+
+ await flushTests()
+
+ server = await runServer(1)
+
+ await setAccessTokensToServers([ server ])
+
+ const res = await uploadVideo(server.url, server.accessToken, {})
+ videoUUID = res.body.video.uuid
+ videoId = res.body.video.id
+ })
+
+ it('Should not have threads on this video', async function () {
+ const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+
+ expect(res.body.total).to.equal(0)
+ expect(res.body.data).to.be.an('array')
+ expect(res.body.data).to.have.lengthOf(0)
+ })
+
+ it('Should create a thread in this video', async function () {
+ const text = 'my super first comment'
+
+ await addVideoCommentThread(server.url, server.accessToken, videoUUID, text)
+ })
+
+ it('Should list threads of this video', async function () {
+ const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+
+ expect(res.body.total).to.equal(1)
+ expect(res.body.data).to.be.an('array')
+ expect(res.body.data).to.have.lengthOf(1)
+
+ const comment: VideoComment = res.body.data[0]
+ expect(comment.inReplyToCommentId).to.be.null
+ expect(comment.text).equal('my super first comment')
+ expect(comment.videoId).to.equal(videoId)
+ expect(comment.id).to.equal(comment.threadId)
+ expect(comment.account.name).to.equal('root')
+ expect(dateIsValid(comment.createdAt as string)).to.be.true
+ expect(dateIsValid(comment.updatedAt as string)).to.be.true
+
+ threadId = comment.threadId
+ })
+
+ it('Should get all the thread created', async function () {
+ const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+
+ const rootComment = res.body.comment
+ expect(rootComment.inReplyToCommentId).to.be.null
+ expect(rootComment.text).equal('my super first comment')
+ expect(rootComment.videoId).to.equal(videoId)
+ expect(dateIsValid(rootComment.createdAt as string)).to.be.true
+ expect(dateIsValid(rootComment.updatedAt as string)).to.be.true
+ })
+
+ it('Should create multiple replies in this thread', async function () {
+ const text1 = 'my super answer to thread 1'
+ const childCommentRes = await addVideoCommentReply(server.url, server.accessToken, videoId, threadId, text1)
+ const childCommentId = childCommentRes.body.comment.id
+
+ const text2 = 'my super answer to answer of thread 1'
+ await addVideoCommentReply(server.url, server.accessToken, videoId, childCommentId, text2)
+
+ const text3 = 'my second answer to thread 1'
+ await addVideoCommentReply(server.url, server.accessToken, videoId, threadId, text3)
+ })
+
+ it('Should get correctly the replies', async function () {
+ const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+
+ const tree: VideoCommentThreadTree = res.body
+ expect(tree.comment.text).equal('my super first comment')
+ expect(tree.children).to.have.lengthOf(2)
+
+ const firstChild = tree.children[0]
+ expect(firstChild.comment.text).to.equal('my super answer to thread 1')
+ expect(firstChild.children).to.have.lengthOf(1)
+
+ const childOfFirstChild = firstChild.children[0]
+ expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
+ expect(childOfFirstChild.children).to.have.lengthOf(0)
+
+ const secondChild = tree.children[1]
+ expect(secondChild.comment.text).to.equal('my second answer to thread 1')
+ expect(secondChild.children).to.have.lengthOf(0)
+ })
+
+ it('Should create other threads', async function () {
+ const text1 = 'super thread 2'
+ await addVideoCommentThread(server.url, server.accessToken, videoUUID, text1)
+
+ const text2 = 'super thread 3'
+ await addVideoCommentThread(server.url, server.accessToken, videoUUID, text2)
+ })
+
+ it('Should list the threads', async function () {
+ const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
+
+ expect(res.body.total).to.equal(3)
+ expect(res.body.data).to.be.an('array')
+ expect(res.body.data).to.have.lengthOf(3)
+
+ expect(res.body.data[0].text).to.equal('my super first comment')
+ expect(res.body.data[1].text).to.equal('super thread 2')
+ expect(res.body.data[2].text).to.equal('super thread 3')
+ })
+
+ after(async function () {
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
--- /dev/null
+import * as request from 'supertest'
+
+function getVideoCommentThreads (url: string, videoId: number, start: number, count: number, sort?: string) {
+ const path = '/api/v1/videos/' + videoId + '/comment-threads'
+
+ const req = request(url)
+ .get(path)
+ .query({ start: start })
+ .query({ count: count })
+
+ if (sort) req.query({ sort })
+
+ return req.set('Accept', 'application/json')
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+function getVideoThreadComments (url: string, videoId: number, threadId: number) {
+ const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
+
+ return request(url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+function addVideoCommentThread (url: string, token: string, videoId: number, text: string, expectedStatus = 200) {
+ const path = '/api/v1/videos/' + videoId + '/comment-threads'
+
+ return request(url)
+ .post(path)
+ .send({ text })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + token)
+ .expect(expectedStatus)
+}
+
+function addVideoCommentReply (
+ url: string,
+ token: string,
+ videoId: number,
+ inReplyToCommentId: number,
+ text: string,
+ expectedStatus = 200
+) {
+ const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
+
+ return request(url)
+ .post(path)
+ .send({ text })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + token)
+ .expect(expectedStatus)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ getVideoCommentThreads,
+ getVideoThreadComments,
+ addVideoCommentThread,
+ addVideoCommentReply
+}
videoId: number
createdAt: Date | string
updatedAt: Date | string
+ account: {
+ name: string
+ }
}
-export interface VideoCommentThread {
+export interface VideoCommentThreadTree {
comment: VideoComment
- children: VideoCommentThread[]
+ children: VideoCommentThreadTree[]
}
export interface VideoCommentCreate {