diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 8243b9f1c..cf6b71b60 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -1,14 +1,10 @@ -import { Account } from '../../../../../shared/models/actors' -import { Video } from '../../shared/video/video.model' -import { AuthUser } from '../../core' import { - VideoDetails as VideoDetailsServerModel, - VideoFile, - VideoChannel, - VideoResolution, - UserRight, - VideoPrivacy + UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile, VideoPrivacy, + VideoResolution } from '../../../../../shared' +import { Account } from '../../../../../shared/models/actors' +import { AuthUser } from '../../core' +import { Video } from '../../shared/video/video.model' export class VideoDetails extends Video implements VideoDetailsServerModel { accountName: string @@ -48,6 +44,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { account: Account likesPercent: number dislikesPercent: number + commentsEnabled: boolean constructor (hash: VideoDetailsServerModel) { super(hash) @@ -59,6 +56,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.channel = hash.channel this.account = hash.account this.tags = hash.tags + this.commentsEnabled = hash.commentsEnabled this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 47c63d976..b1c772217 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts @@ -9,6 +9,7 @@ export class VideoEdit { name: string tags: string[] nsfw: boolean + commentsEnabled: boolean channel: number privacy: VideoPrivacy uuid?: string @@ -25,6 +26,7 @@ export class VideoEdit { this.name = videoDetails.name this.tags = videoDetails.tags this.nsfw = videoDetails.nsfw + this.commentsEnabled = videoDetails.commentsEnabled this.channel = videoDetails.channel.id this.privacy = videoDetails.privacy } @@ -45,6 +47,7 @@ export class VideoEdit { name: this.name, tags: this.tags, nsfw: this.nsfw, + commentsEnabled: this.commentsEnabled, channelId: this.channel, privacy: this.privacy } diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index fc7505a51..073acb2b6 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -55,7 +55,8 @@ export class VideoService { description, privacy: video.privacy, tags: video.tags, - nsfw: video.nsfw + nsfw: video.nsfw, + commentsEnabled: video.commentsEnabled } return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body) diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 9acbafcb6..80377933e 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html @@ -99,5 +99,11 @@ +
+ + + +
+ diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 7fe265284..2b307d5fa 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts @@ -70,6 +70,7 @@ export class VideoEditComponent implements OnInit { this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) this.form.addControl('channelId', new FormControl({ value: '', disabled: true })) this.form.addControl('nsfw', new FormControl(false)) + this.form.addControl('commentsEnabled', new FormControl(true)) this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) this.form.addControl('licence', new FormControl('', VIDEO_LICENCE.VALIDATORS)) this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS)) diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 9bbee58d8..843475647 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -88,6 +88,7 @@ export class VideoAddComponent extends FormReactive implements OnInit { const name = videofile.name.replace(/\.[^/.]+$/, '') const privacy = this.firstStepPrivacyId.toString() const nsfw = false + const commentsEnabled = true const channelId = this.firstStepChannelId.toString() const formData = new FormData() @@ -95,6 +96,7 @@ export class VideoAddComponent extends FormReactive implements OnInit { // Put the video "private" -> we wait he validates the second step formData.append('privacy', VideoPrivacy.PRIVATE.toString()) formData.append('nsfw', '' + nsfw) + formData.append('commentsEnabled', '' + commentsEnabled) formData.append('channelId', '' + channelId) formData.append('videofile', videofile) diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html index 5c6908150..078900e06 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html @@ -3,35 +3,43 @@ Comments - + + -
-
- +
No comments.
-
- View all {{ comment.totalReplies }} replies +
+
+ - - +
+ View all {{ comment.totalReplies }} replies + + + +
+ + +
+ Comments are disabled.
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index f4dda9089..4d801c970 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts @@ -5,6 +5,7 @@ import { AuthService } from '../../../core/auth' import { ComponentPagination } from '../../../shared/rest/component-pagination.model' import { User } from '../../../shared/users' import { SortField } from '../../../shared/video/sort-field.type' +import { VideoDetails } from '../../../shared/video/video-details.model' import { Video } from '../../../shared/video/video.model' import { VideoComment } from './video-comment.model' import { VideoCommentService } from './video-comment.service' @@ -15,7 +16,7 @@ import { VideoCommentService } from './video-comment.service' styleUrls: ['./video-comments.component.scss'] }) export class VideoCommentsComponent implements OnInit { - @Input() video: Video + @Input() video: VideoDetails @Input() user: User comments: VideoComment[] = [] @@ -36,7 +37,9 @@ export class VideoCommentsComponent implements OnInit { ) {} ngOnInit () { - this.loadMoreComments() + if (this.video.commentsEnabled === true) { + this.loadMoreComments() + } } viewReplies (comment: VideoComment) { diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index b11da2ef7..e09b242ed 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts @@ -1,4 +1,5 @@ import * as express from 'express' +import { ResultList } from '../../../../shared/models' import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { getFormattedObjects } from '../../../helpers/utils' @@ -10,6 +11,7 @@ import { addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator, listVideoThreadCommentsValidator } from '../../../middlewares/validators/video-comments' +import { VideoModel } from '../../../models/video/video' import { VideoCommentModel } from '../../../models/video/video-comment' const videoCommentRouter = express.Router() @@ -47,13 +49,33 @@ export { // --------------------------------------------------------------------------- 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) + const video = res.locals.video as VideoModel + let resultList: ResultList + + if (video.commentsEnabled === true) { + resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort) + } else { + resultList = { + total: 0, + data: [] + } + } 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) + const video = res.locals.video as VideoModel + let resultList: ResultList + + if (video.commentsEnabled === true) { + resultList = await VideoCommentModel.listThreadCommentsForApi(res.locals.video.id, res.locals.videoCommentThread.id) + } else { + resultList = { + total: 0, + data: [] + } + } return res.json(buildFormattedCommentTree(resultList)) } diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index ff0d967e1..368327914 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -1,12 +1,11 @@ import * as express from 'express' -import * as multer from 'multer' import { extname, join } from 'path' import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared' import { renamePromise } from '../../../helpers/core-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' import { logger } from '../../../helpers/logger' -import { createReqFiles, generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' +import { createReqFiles, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' import { CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES @@ -141,6 +140,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi category: videoInfo.category, licence: videoInfo.licence, language: videoInfo.language, + commentsEnabled: videoInfo.commentsEnabled, nsfw: videoInfo.nsfw, description: videoInfo.description, privacy: videoInfo.privacy, @@ -248,6 +248,7 @@ async function updateVideo (req: express.Request, res: express.Response) { if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', parseInt(videoInfoToUpdate.privacy.toString(), 10)) if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) + if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index f2e137061..fbdde10ad 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -5,7 +5,6 @@ import { isAnnounceActivityValid } from './announce' import { isActivityPubUrlValid } from './misc' import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isUndoActivityValid } from './undo' -import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' import { isVideoCommentCreateActivityValid } from './video-comments' import { isVideoFlagValid, @@ -65,13 +64,11 @@ function checkCreateActivity (activity: any) { } function checkUpdateActivity (activity: any) { - return isVideoTorrentUpdateActivityValid(activity) || - isVideoChannelUpdateActivityValid(activity) + return isVideoTorrentUpdateActivityValid(activity) } function checkDeleteActivity (activity: any) { return isVideoTorrentDeleteActivityValid(activity) || - isVideoChannelDeleteActivityValid(activity) || isActorDeleteActivityValid(activity) } diff --git a/server/helpers/custom-validators/activitypub/video-channels.ts b/server/helpers/custom-validators/activitypub/video-channels.ts deleted file mode 100644 index eb45c6372..000000000 --- a/server/helpers/custom-validators/activitypub/video-channels.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { isDateValid, isUUIDValid } from '../misc' -import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' - -function isVideoChannelUpdateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Update') && - isVideoChannelObjectValid(activity.object) -} - -function isVideoChannelDeleteActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Delete') -} - -function isVideoChannelObjectValid (videoChannel: any) { - return videoChannel.type === 'VideoChannel' && - isActivityPubUrlValid(videoChannel.id) && - isVideoChannelNameValid(videoChannel.name) && - isVideoChannelDescriptionValid(videoChannel.content) && - isDateValid(videoChannel.published) && - isDateValid(videoChannel.updated) && - isUUIDValid(videoChannel.uuid) -} - -// --------------------------------------------------------------------------- - -export { - isVideoChannelUpdateActivityValid, - isVideoChannelDeleteActivityValid, - isVideoChannelObjectValid -} diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index ae1339611..37cd6965a 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -1,11 +1,10 @@ import * as validator from 'validator' import { ACTIVITY_PUB } from '../../../initializers' -import { exists, isDateValid, isUUIDValid } from '../misc' +import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' import { isVideoAbuseReasonValid, isVideoDurationValid, isVideoNameValid, - isVideoNSFWValid, isVideoTagValid, isVideoTruncatedDescriptionValid, isVideoViewsValid @@ -53,7 +52,8 @@ function isVideoTorrentObjectValid (video: any) { (!video.licence || isRemoteIdentifierValid(video.licence)) && (!video.language || isRemoteIdentifierValid(video.language)) && isVideoViewsValid(video.views) && - isVideoNSFWValid(video.nsfw) && + isBooleanValid(video.nsfw) && + isBooleanValid(video.commentsEnabled) && isDateValid(video.published) && isDateValid(video.updated) && (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 160ec91f3..3903884ea 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -24,6 +24,10 @@ function isIdOrUUIDValid (value: string) { return isIdValid(value) || isUUIDValid(value) } +function isBooleanValid (value: string) { + return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) +} + // --------------------------------------------------------------------------- export { @@ -32,5 +36,6 @@ export { isIdValid, isUUIDValid, isIdOrUUIDValid, - isDateValid + isDateValid, + isBooleanValid } diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index ee9d0ed19..1a5fdb887 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -30,10 +30,6 @@ function isVideoLanguageValid (value: number) { return value === null || VIDEO_LANGUAGES[value] !== undefined } -function isVideoNSFWValid (value: any) { - return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) -} - function isVideoDurationValid (value: string) { return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) } @@ -131,7 +127,6 @@ export { isVideoCategoryValid, isVideoLicenceValid, isVideoLanguageValid, - isVideoNSFWValid, isVideoTruncatedDescriptionValid, isVideoDescriptionValid, isVideoFileInfoHashValid, diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 7a32e286c..b61d6e3fa 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -3,7 +3,7 @@ import * as multer from 'multer' import { Model } from 'sequelize-typescript' import { ResultList } from '../../shared' import { VideoResolution } from '../../shared/models/videos' -import { CONFIG, REMOTE_SCHEME, VIDEO_MIMETYPE_EXT } from '../initializers' +import { CONFIG, REMOTE_SCHEME } from '../initializers' import { UserModel } from '../models/account/user' import { ActorModel } from '../models/activitypub/actor' import { ApplicationModel } from '../models/application/application' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 50a29dc43..31bb6c981 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -9,7 +9,7 @@ import { isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 150 +const LAST_MIGRATION_VERSION = 155 // --------------------------------------------------------------------------- diff --git a/server/initializers/migrations/0155-video-comments-enabled.ts b/server/initializers/migrations/0155-video-comments-enabled.ts new file mode 100644 index 000000000..59f4110de --- /dev/null +++ b/server/initializers/migrations/0155-video-comments-enabled.ts @@ -0,0 +1,26 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { + const data = { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true + } + await utils.queryInterface.addColumn('video', 'commentsEnabled', data) + + data.defaultValue = null + return utils.queryInterface.changeColumn('video', 'commentsEnabled', data) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts index f65395c99..461619ea7 100644 --- a/server/lib/activitypub/process/misc.ts +++ b/server/lib/activitypub/process/misc.ts @@ -53,6 +53,7 @@ async function videoActivityObjectToDBAttributes ( language, description, nsfw: videoObject.nsfw, + commentsEnabled: videoObject.commentsEnabled, channelId: videoChannel.id, duration: parseInt(duration, 10), createdAt: new Date(videoObject.published), diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 42ebddd56..7c77e9a39 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -3,11 +3,10 @@ import 'express-validator' import { body, param } from 'express-validator/check' import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' import { - isAvatarFile, - isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, + isAvatarFile, isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' -import { isVideoExist, isVideoFile } from '../../helpers/custom-validators/videos' +import { isVideoExist } from '../../helpers/custom-validators/videos' import { logger } from '../../helpers/logger' import { isSignupAllowed } from '../../helpers/utils' import { CONSTRAINTS_FIELDS } from '../../initializers' diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/video-comments.ts index fdd092571..ade0b7b9f 100644 --- a/server/middlewares/validators/video-comments.ts +++ b/server/middlewares/validators/video-comments.ts @@ -45,6 +45,7 @@ const addVideoCommentThreadValidator = [ if (areValidationErrors(req, res)) return if (!await isVideoExist(req.params.videoId, res)) return + if (!isVideoCommentsEnabled(res.locals.video, res)) return return next() } @@ -60,6 +61,7 @@ const addVideoCommentReplyValidator = [ if (areValidationErrors(req, res)) return if (!await isVideoExist(req.params.videoId, res)) return + if (!isVideoCommentsEnabled(res.locals.video, res)) return if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return return next() @@ -146,3 +148,15 @@ async function isVideoCommentExist (id: number, video: VideoModel, res: express. res.locals.videoComment = videoComment return true } + +function isVideoCommentsEnabled (video: VideoModel, res: express.Response) { + if (video.commentsEnabled !== true) { + res.status(409) + .json({ error: 'Video comments are disabled for this video.' }) + .end() + + return false + } + + return true +} diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index bffc50322..e8cb2ae03 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -2,10 +2,10 @@ import * as express from 'express' import 'express-validator' import { body, param, query } from 'express-validator/check' import { UserRight, VideoPrivacy } from '../../../shared' -import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' +import { isBooleanValid, isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' import { isVideoAbuseReasonValid, isVideoCategoryValid, isVideoDescriptionValid, isVideoExist, isVideoFile, isVideoLanguageValid, - isVideoLicenceValid, isVideoNameValid, isVideoNSFWValid, isVideoPrivacyValid, isVideoRatingTypeValid, isVideoTagsValid + isVideoLicenceValid, isVideoNameValid, isVideoPrivacyValid, isVideoRatingTypeValid, isVideoTagsValid } from '../../helpers/custom-validators/videos' import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' import { logger } from '../../helpers/logger' @@ -26,11 +26,12 @@ const videosAddValidator = [ body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), - body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), + body('nsfw').custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'), body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), + body('commentsEnabled').custom(isBooleanValid).withMessage('Should have comments enabled boolean'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) @@ -85,10 +86,11 @@ const videosUpdateValidator = [ body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), - body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), + body('nsfw').optional().custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), body('privacy').optional().custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), + body('commentsEnabled').optional().custom(isBooleanValid).withMessage('Should have comments enabled boolean'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosUpdate parameters', { parameters: req.body }) diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 8422653df..a12f3ec9e 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -1,5 +1,5 @@ import { values } from 'lodash' -import { extname, join } from 'path' +import { extname } from 'path' import * as Sequelize from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes, @@ -13,7 +13,7 @@ import { isActorPublicKeyValid } from '../../helpers/custom-validators/activitypub/actor' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' +import { ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { AccountModel } from '../account/account' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts index 7493c3d75..e1d4c20bc 100644 --- a/server/models/avatar/avatar.ts +++ b/server/models/avatar/avatar.ts @@ -2,9 +2,7 @@ import { join } from 'path' import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' import { Avatar } from '../../../shared/models/avatars/avatar.model' import { unlinkPromise } from '../../helpers/core-utils' -import { logger } from '../../helpers/logger' import { CONFIG, STATIC_PATHS } from '../../initializers' -import { sendDeleteVideo } from '../../lib/activitypub/send' @Table({ tableName: 'avatar' diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 2504ae58a..c4b716cd2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -15,9 +15,10 @@ import { Video, VideoDetails } from '../../../shared/models/videos' import { activityPubCollection } from '../../helpers/activitypub' import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' +import { isBooleanValid } from '../../helpers/custom-validators/misc' import { isVideoCategoryValid, isVideoDescriptionValid, isVideoDurationValid, isVideoLanguageValid, isVideoLicenceValid, isVideoNameValid, - isVideoNSFWValid, isVideoPrivacyValid + isVideoPrivacyValid } from '../../helpers/custom-validators/videos' import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' import { logger } from '../../helpers/logger' @@ -185,7 +186,7 @@ export class VideoModel extends Model { privacy: number @AllowNull(false) - @Is('VideoNSFW', value => throwIfNotValid(value, isVideoNSFWValid, 'NSFW boolean')) + @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean')) @Column nsfw: boolean @@ -230,6 +231,10 @@ export class VideoModel extends Model { @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) url: string + @AllowNull(false) + @Column + commentsEnabled: boolean + @CreatedAt createdAt: Date @@ -773,6 +778,7 @@ export class VideoModel extends Model { channel: this.VideoChannel.toFormattedJSON(), account: this.VideoChannel.Account.toFormattedJSON(), tags: map(this.Tags, 'name'), + commentsEnabled: this.commentsEnabled, files: [] } @@ -920,6 +926,7 @@ export class VideoModel extends Model { language, views: this.views, nsfw: this.nsfw, + commentsEnabled: this.commentsEnabled, published: this.createdAt.toISOString(), updated: this.updatedAt.toISOString(), mediaType: 'text/markdown', diff --git a/server/tests/activitypub.ts b/server/tests/activitypub.ts index 94615c63f..c8884719d 100644 --- a/server/tests/activitypub.ts +++ b/server/tests/activitypub.ts @@ -20,11 +20,11 @@ describe('Test activitypub', function () { }) it('Should return the account object', async function () { - const res = await makeActivityPubGetRequest(server.url, '/account/root') + const res = await makeActivityPubGetRequest(server.url, '/accounts/root') const object = res.body expect(object.type).to.equal('Person') - expect(object.id).to.equal('http://localhost:9001/account/root') + expect(object.id).to.equal('http://localhost:9001/accounts/root') expect(object.name).to.equal('root') expect(object.preferredUsername).to.equal('root') }) diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 44412ad82..33d92ac24 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -2,14 +2,13 @@ import { omit } from 'lodash' import 'mocha' -import { join } from "path" +import { join } from 'path' import { UserRole } from '../../../../shared' import { createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, makePostBodyRequest, makePostUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, - updateUser, - uploadVideo, userLogin + updateUser, uploadVideo, userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' @@ -25,7 +24,7 @@ describe('Test users API validators', function () { // --------------------------------------------------------------- before(async function () { - this.timeout(120000) + this.timeout(20000) await flushTests() @@ -282,7 +281,14 @@ describe('Test users API validators', function () { const attaches = { 'avatarfile': join(__dirname, '..', 'fixtures', 'avatar.png') } - await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) + await makePostUploadRequest({ + url: server.url, + path: path + '/me/avatar/pick', + token: server.accessToken, + fields, + attaches, + statusCodeExpected: 200 + }) }) }) diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts index cdb48a276..c11660d07 100644 --- a/server/tests/api/check-params/video-comments.ts +++ b/server/tests/api/check-params/video-comments.ts @@ -1,5 +1,6 @@ /* tslint:disable:no-unused-expression */ +import * as chai from 'chai' import 'mocha' import { flushTests, killallServers, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, @@ -8,6 +9,8 @@ import { import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' import { addVideoCommentThread } from '../../utils/videos/video-comments' +const expect = chai.expect + describe('Test video comments API validator', function () { let pathThread: string let pathComment: string @@ -42,17 +45,14 @@ describe('Test video comments API validator', function () { describe('When listing video comment threads', function () { it('Should fail with a bad start pagination', async function () { await checkBadStartPagination(server.url, pathThread, server.accessToken) - }) it('Should fail with a bad count pagination', async function () { await checkBadCountPagination(server.url, pathThread, server.accessToken) - }) it('Should fail with an incorrect sort', async function () { await checkBadSortPagination(server.url, pathThread, server.accessToken) - }) it('Should fail with an incorrect video', async function () { @@ -185,6 +185,35 @@ describe('Test video comments API validator', function () { }) }) + describe('When a video has comments disabled', function () { + before(async function () { + const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false }) + videoUUID = res.body.video.uuid + pathThread = '/api/v1/videos/' + videoUUID + '/comment-threads' + }) + + it('Should return an empty thread list', async function () { + const res = await makeGetRequest({ + url: server.url, + path: pathThread, + statusCodeExpected: 200 + }) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + }) + + it('Should return an thread comments list') + + it('Should return conflict on thread add', async function () { + const fields = { + text: 'super comment' + } + await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields, statusCodeExpected: 409 }) + }) + + it('Should return conflict on comment thread add') + }) + after(async function () { killallServers([ server ]) diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index b9484afc4..5c067dc96 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -100,6 +100,7 @@ describe('Test videos API validator', function () { licence: 1, language: 6, nsfw: false, + commentsEnabled: true, description: 'my super description', tags: [ 'tag1', 'tag2' ], privacy: VideoPrivacy.PUBLIC, @@ -162,6 +163,20 @@ describe('Test videos API validator', function () { await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) + it('Should fail without commentsEnabled attribute', async function () { + const fields = omit(baseCorrectParams, 'commentsEnabled') + const attaches = baseCorrectAttaches + + await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a bad commentsEnabled attribute', async function () { + const fields = immutableAssign(baseCorrectParams, { commentsEnabled: 2 }) + const attaches = baseCorrectAttaches + + await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + it('Should fail with a long description', async function () { const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(1500) }) const attaches = baseCorrectAttaches @@ -291,6 +306,7 @@ describe('Test videos API validator', function () { licence: 2, language: 6, nsfw: false, + commentsEnabled: false, description: 'my super description', privacy: VideoPrivacy.PUBLIC, tags: [ 'tag1', 'tag2' ] @@ -354,6 +370,12 @@ describe('Test videos API validator', function () { await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) }) + it('Should fail with a bad commentsEnabled attribute', async function () { + const fields = immutableAssign(baseCorrectParams, { commentsEnabled: 2 }) + + await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) + }) + it('Should fail with a long description', async function () { const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(1500) }) diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index fc9c5c3b6..e6dfd5f62 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts @@ -246,6 +246,7 @@ describe('Test follows', function () { host: 'localhost:9003', account: 'root', isLocal, + commentsEnabled: true, duration: 5, tags: [ 'tag1', 'tag2', 'tag3' ], privacy: VideoPrivacy.PUBLIC, diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index abd051a30..b6dfe0d1b 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -93,6 +93,7 @@ describe('Test multiple servers', function () { duration: 10, tags: [ 'tag1p1', 'tag2p1' ], privacy: VideoPrivacy.PUBLIC, + commentsEnabled: true, channel: { name: 'my channel', description: 'super channel', @@ -155,6 +156,7 @@ describe('Test multiple servers', function () { host: 'localhost:9002', account: 'user1', isLocal, + commentsEnabled: true, duration: 5, tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], privacy: VideoPrivacy.PUBLIC, @@ -254,6 +256,7 @@ describe('Test multiple servers', function () { account: 'root', isLocal, duration: 5, + commentsEnabled: true, tags: [ 'tag1p3' ], privacy: VideoPrivacy.PUBLIC, channel: { @@ -280,6 +283,7 @@ describe('Test multiple servers', function () { description: 'my super description for server 3-2', host: 'localhost:9003', account: 'root', + commentsEnabled: true, isLocal, duration: 5, tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], @@ -545,6 +549,7 @@ describe('Test multiple servers', function () { account: 'root', isLocal, duration: 5, + commentsEnabled: true, tags: [ 'tag_up_1', 'tag_up_2' ], privacy: VideoPrivacy.PUBLIC, channel: { @@ -732,6 +737,26 @@ describe('Test multiple servers', function () { expect(secondChild.children).to.have.lengthOf(0) } }) + + it('Should disable comments', async function () { + this.timeout(20000) + + const attributes = { + commentsEnabled: false + } + + await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes) + + await wait(5000) + + for (const server of servers) { + const res = await getVideo(server.url, videoUUID) + expect(res.body.commentsEnabled).to.be.false + + const text = 'my super forbidden comment' + await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, 409) + } + }) }) describe('With minimum parameters', function () { @@ -748,6 +773,7 @@ describe('Test multiple servers', function () { .field('privacy', '1') .field('nsfw', 'false') .field('channelId', '1') + .field('commentsEnabled', 'true') const filePath = join(__dirname, '..', '..', 'api', 'fixtures', 'video_short.webm') @@ -772,6 +798,7 @@ describe('Test multiple servers', function () { account: 'root', isLocal, duration: 5, + commentsEnabled: true, tags: [ ], privacy: VideoPrivacy.PUBLIC, channel: { diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 2a3126f32..0a0c95750 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts @@ -32,6 +32,7 @@ describe('Test a single server', function () { duration: 5, tags: [ 'tag1', 'tag2', 'tag3' ], privacy: VideoPrivacy.PUBLIC, + commentsEnabled: true, channel: { name: 'Default root channel', description: '', @@ -51,7 +52,7 @@ describe('Test a single server', function () { category: 4, licence: 2, language: 5, - nsfw: true, + nsfw: false, description: 'my super description updated', host: 'localhost:9001', account: 'root', @@ -59,6 +60,7 @@ describe('Test a single server', function () { tags: [ 'tagup1', 'tagup2' ], privacy: VideoPrivacy.PUBLIC, duration: 5, + commentsEnabled: false, channel: { name: 'Default root channel', description: '', @@ -475,6 +477,7 @@ describe('Test a single server', function () { language: 5, nsfw: false, description: 'my super description updated', + commentsEnabled: false, tags: [ 'tagup1', 'tagup2' ] } await updateVideo(server.url, server.accessToken, videoId, attributes) diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index aca51ee5d..c437c21b2 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -16,6 +16,7 @@ type VideoAttributes = { licence?: number language?: number nsfw?: boolean + commentsEnabled?: boolean description?: string tags?: string[] channelId?: number @@ -238,6 +239,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg description: 'my super description', tags: [ 'tag' ], privacy: VideoPrivacy.PUBLIC, + commentsEnabled: true, fixture: 'video_short.webm' } attributes = Object.assign(attributes, videoAttributesArg) @@ -250,6 +252,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg .field('category', attributes.category.toString()) .field('licence', attributes.licence.toString()) .field('nsfw', JSON.stringify(attributes.nsfw)) + .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) .field('description', attributes.description) .field('privacy', attributes.privacy.toString()) .field('channelId', attributes.channelId) @@ -273,7 +276,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg .expect(specialStatus) } -function updateVideo (url: string, accessToken: string, id: number, attributes: VideoAttributes, specialStatus = 204) { +function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, specialStatus = 204) { const path = '/api/v1/videos/' + id const body = {} @@ -281,7 +284,8 @@ function updateVideo (url: string, accessToken: string, id: number, attributes: if (attributes.category) body['category'] = attributes.category if (attributes.licence) body['licence'] = attributes.licence if (attributes.language) body['language'] = attributes.language - if (attributes.nsfw) body['nsfw'] = attributes.nsfw + if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw) + if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled) if (attributes.description) body['description'] = attributes.description if (attributes.tags) body['tags'] = attributes.tags if (attributes.privacy) body['privacy'] = attributes.privacy @@ -326,6 +330,7 @@ async function completeVideoCheck ( licence: number language: number nsfw: boolean + commentsEnabled: boolean description: string host: string account: string @@ -376,6 +381,7 @@ async function completeVideoCheck ( expect(videoDetails.privacy).to.deep.equal(attributes.privacy) expect(videoDetails.privacyLabel).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) expect(videoDetails.account.name).to.equal(attributes.account) + expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) expect(videoDetails.channel.name).to.equal(attributes.channel.name) expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts index 5ccc80bcb..cf0e0ba54 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-torrent-object.ts @@ -18,6 +18,7 @@ export interface VideoTorrentObject { language: ActivityIdentifierObject views: number nsfw: boolean + commentsEnabled: boolean published: string updated: string mediaType: 'text/markdown' diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 8bc6a6639..139c2579e 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts @@ -9,5 +9,6 @@ export interface VideoCreate { nsfw: boolean name: string tags?: string[] + commentsEnabled?: boolean privacy: VideoPrivacy } diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts index 0cf38fe6e..fc772f77b 100644 --- a/shared/models/videos/video-update.model.ts +++ b/shared/models/videos/video-update.model.ts @@ -8,5 +8,6 @@ export interface VideoUpdate { description?: string privacy?: VideoPrivacy tags?: string[] + commentsEnabled?: boolean nsfw?: boolean } diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 13b9c49b3..39d1edc06 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -45,4 +45,5 @@ export interface VideoDetails extends Video { tags: string[] files: VideoFile[] account: Account + commentsEnabled: boolean }