diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/videos/comment.ts | 88 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/activity.ts | 4 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/video-comments.ts | 40 | ||||
-rw-r--r-- | server/initializers/constants.ts | 3 | ||||
-rw-r--r-- | server/initializers/database.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 53 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 95 | ||||
-rw-r--r-- | server/models/video/video.ts | 12 |
8 files changed, 295 insertions, 4 deletions
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts new file mode 100644 index 000000000..b69aa5d40 --- /dev/null +++ b/server/controllers/api/videos/comment.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | // import * as express from 'express' | ||
2 | // import { logger, getFormattedObjects } from '../../../helpers' | ||
3 | // import { | ||
4 | // authenticate, | ||
5 | // ensureUserHasRight, | ||
6 | // videosBlacklistAddValidator, | ||
7 | // videosBlacklistRemoveValidator, | ||
8 | // paginationValidator, | ||
9 | // blacklistSortValidator, | ||
10 | // setBlacklistSort, | ||
11 | // setPagination, | ||
12 | // asyncMiddleware | ||
13 | // } from '../../../middlewares' | ||
14 | // import { BlacklistedVideo, UserRight } from '../../../../shared' | ||
15 | // import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | ||
16 | // | ||
17 | // const videoCommentRouter = express.Router() | ||
18 | // | ||
19 | // videoCommentRouter.get('/:videoId/comment', | ||
20 | // authenticate, | ||
21 | // ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
22 | // asyncMiddleware(listVideoCommentsThreadsValidator), | ||
23 | // asyncMiddleware(listVideoCommentsThreads) | ||
24 | // ) | ||
25 | // | ||
26 | // videoCommentRouter.post('/:videoId/comment', | ||
27 | // authenticate, | ||
28 | // ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
29 | // asyncMiddleware(videosBlacklistAddValidator), | ||
30 | // asyncMiddleware(addVideoToBlacklist) | ||
31 | // ) | ||
32 | // | ||
33 | // videoCommentRouter.get('/blacklist', | ||
34 | // authenticate, | ||
35 | // ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
36 | // paginationValidator, | ||
37 | // blacklistSortValidator, | ||
38 | // setBlacklistSort, | ||
39 | // setPagination, | ||
40 | // asyncMiddleware(listBlacklist) | ||
41 | // ) | ||
42 | // | ||
43 | // videoCommentRouter.delete('/:videoId/blacklist', | ||
44 | // authenticate, | ||
45 | // ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
46 | // asyncMiddleware(videosBlacklistRemoveValidator), | ||
47 | // asyncMiddleware(removeVideoFromBlacklistController) | ||
48 | // ) | ||
49 | // | ||
50 | // // --------------------------------------------------------------------------- | ||
51 | // | ||
52 | // export { | ||
53 | // videoCommentRouter | ||
54 | // } | ||
55 | // | ||
56 | // // --------------------------------------------------------------------------- | ||
57 | // | ||
58 | // async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
59 | // const videoInstance = res.locals.video | ||
60 | // | ||
61 | // const toCreate = { | ||
62 | // videoId: videoInstance.id | ||
63 | // } | ||
64 | // | ||
65 | // await VideoBlacklistModel.create(toCreate) | ||
66 | // return res.type('json').status(204).end() | ||
67 | // } | ||
68 | // | ||
69 | // async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
70 | // const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) | ||
71 | // | ||
72 | // return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total)) | ||
73 | // } | ||
74 | // | ||
75 | // async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
76 | // const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel | ||
77 | // | ||
78 | // try { | ||
79 | // await blacklistedVideo.destroy() | ||
80 | // | ||
81 | // logger.info('Video %s removed from blacklist.', res.locals.video.uuid) | ||
82 | // | ||
83 | // return res.sendStatus(204) | ||
84 | // } catch (err) { | ||
85 | // logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, err) | ||
86 | // throw err | ||
87 | // } | ||
88 | // } | ||
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index c402800a4..f2e137061 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -6,6 +6,7 @@ import { isActivityPubUrlValid } from './misc' | |||
6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | 6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' |
7 | import { isUndoActivityValid } from './undo' | 7 | import { isUndoActivityValid } from './undo' |
8 | import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' | 8 | import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' |
9 | import { isVideoCommentCreateActivityValid } from './video-comments' | ||
9 | import { | 10 | import { |
10 | isVideoFlagValid, | 11 | isVideoFlagValid, |
11 | isVideoTorrentCreateActivityValid, | 12 | isVideoTorrentCreateActivityValid, |
@@ -59,7 +60,8 @@ function checkCreateActivity (activity: any) { | |||
59 | return isViewActivityValid(activity) || | 60 | return isViewActivityValid(activity) || |
60 | isDislikeActivityValid(activity) || | 61 | isDislikeActivityValid(activity) || |
61 | isVideoTorrentCreateActivityValid(activity) || | 62 | isVideoTorrentCreateActivityValid(activity) || |
62 | isVideoFlagValid(activity) | 63 | isVideoFlagValid(activity) || |
64 | isVideoCommentCreateActivityValid(activity) | ||
63 | } | 65 | } |
64 | 66 | ||
65 | function checkUpdateActivity (activity: any) { | 67 | function checkUpdateActivity (activity: any) { |
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts new file mode 100644 index 000000000..489ff27de --- /dev/null +++ b/server/helpers/custom-validators/activitypub/video-comments.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import * as validator from 'validator' | ||
2 | import { exists, isDateValid } from '../misc' | ||
3 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | ||
4 | import * as sanitizeHtml from 'sanitize-html' | ||
5 | |||
6 | function isVideoCommentCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | isVideoCommentObjectValid(activity.object) | ||
9 | } | ||
10 | |||
11 | function isVideoCommentObjectValid (comment: any) { | ||
12 | return comment.type === 'Note' && | ||
13 | isActivityPubUrlValid(comment.id) && | ||
14 | sanitizeCommentHTML(comment) && | ||
15 | isCommentContentValid(comment.content) && | ||
16 | isActivityPubUrlValid(comment.inReplyTo) && | ||
17 | isDateValid(comment.published) && | ||
18 | isActivityPubUrlValid(comment.url) | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | export { | ||
24 | isVideoCommentCreateActivityValid | ||
25 | } | ||
26 | |||
27 | // --------------------------------------------------------------------------- | ||
28 | |||
29 | function sanitizeCommentHTML (comment: any) { | ||
30 | return sanitizeHtml(comment.content, { | ||
31 | allowedTags: [ 'b', 'i', 'em', 'span', 'a' ], | ||
32 | allowedAttributes: { | ||
33 | 'a': [ 'href' ] | ||
34 | } | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | function isCommentContentValid (content: any) { | ||
39 | return exists(content) && validator.isLength('' + content, { min: 1 }) | ||
40 | } | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 100a77622..c8b21d10d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -175,6 +175,9 @@ const CONSTRAINTS_FIELDS = { | |||
175 | }, | 175 | }, |
176 | VIDEO_EVENTS: { | 176 | VIDEO_EVENTS: { |
177 | COUNT: { min: 0 } | 177 | COUNT: { min: 0 } |
178 | }, | ||
179 | COMMENT: { | ||
180 | URL: { min: 3, max: 2000 } // Length | ||
178 | } | 181 | } |
179 | } | 182 | } |
180 | 183 | ||
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 0b3f695f7..852db68a0 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -18,6 +18,7 @@ import { VideoModel } from '../models/video/video' | |||
18 | import { VideoAbuseModel } from '../models/video/video-abuse' | 18 | import { VideoAbuseModel } from '../models/video/video-abuse' |
19 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 19 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
20 | import { VideoChannelModel } from '../models/video/video-channel' | 20 | import { VideoChannelModel } from '../models/video/video-channel' |
21 | import { VideoCommentModel } from '../models/video/video-comment' | ||
21 | import { VideoFileModel } from '../models/video/video-file' | 22 | import { VideoFileModel } from '../models/video/video-file' |
22 | import { VideoShareModel } from '../models/video/video-share' | 23 | import { VideoShareModel } from '../models/video/video-share' |
23 | import { VideoTagModel } from '../models/video/video-tag' | 24 | import { VideoTagModel } from '../models/video/video-tag' |
@@ -73,7 +74,8 @@ async function initDatabaseModels (silent: boolean) { | |||
73 | VideoFileModel, | 74 | VideoFileModel, |
74 | VideoBlacklistModel, | 75 | VideoBlacklistModel, |
75 | VideoTagModel, | 76 | VideoTagModel, |
76 | VideoModel | 77 | VideoModel, |
78 | VideoCommentModel | ||
77 | ]) | 79 | ]) |
78 | 80 | ||
79 | if (!silent) logger.info('Database %s is ready.', dbname) | 81 | if (!silent) logger.info('Database %s is ready.', dbname) |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1ddd817db..102e54b19 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { ActivityCreate, VideoTorrentObject } from '../../../../shared' | 2 | import { ActivityCreate, VideoTorrentObject } from '../../../../shared' |
3 | import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' | 3 | import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' |
4 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | ||
4 | import { VideoRateType } from '../../../../shared/models/videos' | 5 | import { VideoRateType } from '../../../../shared/models/videos' |
5 | import { logger, retryTransactionWrapper } from '../../../helpers' | 6 | import { logger, retryTransactionWrapper } from '../../../helpers' |
6 | import { sequelizeTypescript } from '../../../initializers' | 7 | import { sequelizeTypescript } from '../../../initializers' |
@@ -9,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
9 | import { TagModel } from '../../../models/video/tag' | 10 | import { TagModel } from '../../../models/video/tag' |
10 | import { VideoModel } from '../../../models/video/video' | 11 | import { VideoModel } from '../../../models/video/video' |
11 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 12 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
13 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
12 | import { VideoFileModel } from '../../../models/video/video-file' | 14 | import { VideoFileModel } from '../../../models/video/video-file' |
13 | import { getOrCreateActorAndServerAndModel } from '../actor' | 15 | import { getOrCreateActorAndServerAndModel } from '../actor' |
14 | import { forwardActivity } from '../send/misc' | 16 | import { forwardActivity } from '../send/misc' |
@@ -28,6 +30,8 @@ async function processCreateActivity (activity: ActivityCreate) { | |||
28 | return processCreateVideo(actor, activity) | 30 | return processCreateVideo(actor, activity) |
29 | } else if (activityType === 'Flag') { | 31 | } else if (activityType === 'Flag') { |
30 | return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject) | 32 | return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject) |
33 | } else if (activityType === 'Note') { | ||
34 | return processCreateVideoComment(actor, activity) | ||
31 | } | 35 | } |
32 | 36 | ||
33 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 37 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -184,7 +188,7 @@ function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) { | |||
184 | }) | 188 | }) |
185 | } | 189 | } |
186 | 190 | ||
187 | async function processCreateView (byAccount: ActorModel, activity: ActivityCreate) { | 191 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { |
188 | const view = activity.object as ViewObject | 192 | const view = activity.object as ViewObject |
189 | 193 | ||
190 | const video = await VideoModel.loadByUrlAndPopulateAccount(view.object) | 194 | const video = await VideoModel.loadByUrlAndPopulateAccount(view.object) |
@@ -198,7 +202,7 @@ async function processCreateView (byAccount: ActorModel, activity: ActivityCreat | |||
198 | 202 | ||
199 | if (video.isOwned()) { | 203 | if (video.isOwned()) { |
200 | // Don't resend the activity to the sender | 204 | // Don't resend the activity to the sender |
201 | const exceptions = [ byAccount ] | 205 | const exceptions = [ byActor ] |
202 | await forwardActivity(activity, undefined, exceptions) | 206 | await forwardActivity(activity, undefined, exceptions) |
203 | } | 207 | } |
204 | } | 208 | } |
@@ -236,3 +240,48 @@ function addRemoteVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAb | |||
236 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) | 240 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) |
237 | }) | 241 | }) |
238 | } | 242 | } |
243 | |||
244 | function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
245 | const options = { | ||
246 | arguments: [ byActor, activity ], | ||
247 | errorMessage: 'Cannot create video comment with many retries.' | ||
248 | } | ||
249 | |||
250 | return retryTransactionWrapper(createVideoComment, options) | ||
251 | } | ||
252 | |||
253 | function createVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
254 | const comment = activity.object as VideoCommentObject | ||
255 | const byAccount = byActor.Account | ||
256 | |||
257 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) | ||
258 | |||
259 | return sequelizeTypescript.transaction(async t => { | ||
260 | const video = await VideoModel.loadByUrl(comment.inReplyTo, t) | ||
261 | |||
262 | // This is a new thread | ||
263 | if (video) { | ||
264 | return VideoCommentModel.create({ | ||
265 | url: comment.id, | ||
266 | text: comment.content, | ||
267 | originCommentId: null, | ||
268 | inReplyToComment: null, | ||
269 | videoId: video.id, | ||
270 | actorId: byActor.id | ||
271 | }, { transaction: t }) | ||
272 | } | ||
273 | |||
274 | const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t) | ||
275 | if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo) | ||
276 | |||
277 | const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id | ||
278 | return VideoCommentModel.create({ | ||
279 | url: comment.id, | ||
280 | text: comment.content, | ||
281 | originCommentId, | ||
282 | inReplyToCommentId: inReplyToComment.id, | ||
283 | videoId: inReplyToComment.videoId, | ||
284 | actorId: byActor.id | ||
285 | }, { transaction: t }) | ||
286 | }) | ||
287 | } | ||
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts new file mode 100644 index 000000000..92c0c6112 --- /dev/null +++ b/server/models/video/video-comment.ts | |||
@@ -0,0 +1,95 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table, | ||
4 | UpdatedAt | ||
5 | } from 'sequelize-typescript' | ||
6 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' | ||
7 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
8 | import { ActorModel } from '../activitypub/actor' | ||
9 | import { throwIfNotValid } from '../utils' | ||
10 | import { VideoModel } from './video' | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'videoComment', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: [ 'videoId' ] | ||
17 | } | ||
18 | ] | ||
19 | }) | ||
20 | export class VideoCommentModel extends Model<VideoCommentModel> { | ||
21 | @CreatedAt | ||
22 | createdAt: Date | ||
23 | |||
24 | @UpdatedAt | ||
25 | updatedAt: Date | ||
26 | |||
27 | @AllowNull(false) | ||
28 | @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
29 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | ||
30 | url: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column(DataType.TEXT) | ||
34 | text: string | ||
35 | |||
36 | @ForeignKey(() => VideoCommentModel) | ||
37 | @Column | ||
38 | originCommentId: number | ||
39 | |||
40 | @BelongsTo(() => VideoCommentModel, { | ||
41 | foreignKey: { | ||
42 | allowNull: true | ||
43 | }, | ||
44 | onDelete: 'CASCADE' | ||
45 | }) | ||
46 | OriginVideoComment: VideoCommentModel | ||
47 | |||
48 | @ForeignKey(() => VideoCommentModel) | ||
49 | @Column | ||
50 | inReplyToCommentId: number | ||
51 | |||
52 | @BelongsTo(() => VideoCommentModel, { | ||
53 | foreignKey: { | ||
54 | allowNull: true | ||
55 | }, | ||
56 | onDelete: 'CASCADE' | ||
57 | }) | ||
58 | InReplyToVideoComment: VideoCommentModel | ||
59 | |||
60 | @ForeignKey(() => VideoModel) | ||
61 | @Column | ||
62 | videoId: number | ||
63 | |||
64 | @BelongsTo(() => VideoModel, { | ||
65 | foreignKey: { | ||
66 | allowNull: false | ||
67 | }, | ||
68 | onDelete: 'CASCADE' | ||
69 | }) | ||
70 | Video: VideoModel | ||
71 | |||
72 | @ForeignKey(() => ActorModel) | ||
73 | @Column | ||
74 | actorId: number | ||
75 | |||
76 | @BelongsTo(() => ActorModel, { | ||
77 | foreignKey: { | ||
78 | allowNull: false | ||
79 | }, | ||
80 | onDelete: 'CASCADE' | ||
81 | }) | ||
82 | Actor: ActorModel | ||
83 | |||
84 | static loadByUrl (url: string, t?: Sequelize.Transaction) { | ||
85 | const query: IFindOptions<VideoCommentModel> = { | ||
86 | where: { | ||
87 | url | ||
88 | } | ||
89 | } | ||
90 | |||
91 | if (t !== undefined) query.transaction = t | ||
92 | |||
93 | return VideoCommentModel.findOne(query) | ||
94 | } | ||
95 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8c49bc3af..b6a2ce6b5 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -491,6 +491,18 @@ export class VideoModel extends Model<VideoModel> { | |||
491 | return VideoModel.findById(id) | 491 | return VideoModel.findById(id) |
492 | } | 492 | } |
493 | 493 | ||
494 | static loadByUrl (url: string, t?: Sequelize.Transaction) { | ||
495 | const query: IFindOptions<VideoModel> = { | ||
496 | where: { | ||
497 | url | ||
498 | } | ||
499 | } | ||
500 | |||
501 | if (t !== undefined) query.transaction = t | ||
502 | |||
503 | return VideoModel.findOne(query) | ||
504 | } | ||
505 | |||
494 | static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { | 506 | static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { |
495 | const query: IFindOptions<VideoModel> = { | 507 | const query: IFindOptions<VideoModel> = { |
496 | where: { | 508 | where: { |