aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/comment.ts36
-rw-r--r--server/controllers/api/videos/index.ts36
-rw-r--r--server/helpers/core-utils.ts2
-rw-r--r--server/lib/activitypub/video-comments.ts2
-rw-r--r--server/lib/activitypub/videos.ts132
-rw-r--r--server/lib/moderation.ts64
-rw-r--r--server/lib/plugins/hooks.ts26
-rw-r--r--server/lib/plugins/plugin-manager.ts22
-rw-r--r--server/lib/video-blacklist.ts25
-rw-r--r--server/middlewares/validators/videos/video-comments.ts38
-rw-r--r--server/middlewares/validators/videos/videos.ts37
-rw-r--r--server/models/video/video-comment.ts18
12 files changed, 326 insertions, 112 deletions
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 176ee8bd4..a95392543 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
27import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
28import { Notifier } from '../../../lib/notifier' 28import { Notifier } from '../../../lib/notifier'
29import { Hooks } from '../../../lib/plugins/hooks'
29 30
30const auditLogger = auditLoggerFactory('comments') 31const auditLogger = auditLoggerFactory('comments')
31const videoCommentRouter = express.Router() 32const videoCommentRouter = express.Router()
@@ -76,7 +77,18 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
76 let resultList: ResultList<VideoCommentModel> 77 let resultList: ResultList<VideoCommentModel>
77 78
78 if (video.commentsEnabled === true) { 79 if (video.commentsEnabled === true) {
79 resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user) 80 const apiOptions = await Hooks.wrapObject({
81 videoId: video.id,
82 start: req.query.start,
83 count: req.query.count,
84 sort: req.query.sort,
85 user: user
86 }, 'filter:api.video-threads.list.params')
87
88 resultList = await Hooks.wrapPromise(
89 VideoCommentModel.listThreadsForApi(apiOptions),
90 'filter:api.video-threads.list.result'
91 )
80 } else { 92 } else {
81 resultList = { 93 resultList = {
82 total: 0, 94 total: 0,
@@ -94,7 +106,16 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
94 let resultList: ResultList<VideoCommentModel> 106 let resultList: ResultList<VideoCommentModel>
95 107
96 if (video.commentsEnabled === true) { 108 if (video.commentsEnabled === true) {
97 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user) 109 const apiOptions = await Hooks.wrapObject({
110 videoId: video.id,
111 threadId: res.locals.videoCommentThread.id,
112 user: user
113 }, 'filter:api.video-thread-comments.list.params')
114
115 resultList = await Hooks.wrapPromise(
116 VideoCommentModel.listThreadCommentsForApi(apiOptions),
117 'filter:api.video-thread-comments.list.result'
118 )
98 } else { 119 } else {
99 resultList = { 120 resultList = {
100 total: 0, 121 total: 0,
@@ -122,6 +143,8 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
122 Notifier.Instance.notifyOnNewComment(comment) 143 Notifier.Instance.notifyOnNewComment(comment)
123 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) 144 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
124 145
146 Hooks.runAction('action:api.video-thread.created', { comment })
147
125 return res.json({ 148 return res.json({
126 comment: comment.toFormattedJSON() 149 comment: comment.toFormattedJSON()
127 }).end() 150 }).end()
@@ -144,6 +167,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
144 Notifier.Instance.notifyOnNewComment(comment) 167 Notifier.Instance.notifyOnNewComment(comment)
145 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) 168 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
146 169
170 Hooks.runAction('action:api.video-comment-reply.created', { comment })
171
147 return res.json({ comment: comment.toFormattedJSON() }).end() 172 return res.json({ comment: comment.toFormattedJSON() }).end()
148} 173}
149 174
@@ -154,11 +179,10 @@ async function removeVideoComment (req: express.Request, res: express.Response)
154 await videoCommentInstance.destroy({ transaction: t }) 179 await videoCommentInstance.destroy({ transaction: t })
155 }) 180 })
156 181
157 auditLogger.delete( 182 auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
158 getAuditIdFromRes(res),
159 new CommentAuditView(videoCommentInstance.toFormattedJSON())
160 )
161 logger.info('Video comment %d deleted.', videoCommentInstance.id) 183 logger.info('Video comment %d deleted.', videoCommentInstance.id)
162 184
185 Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstance })
186
163 return res.type('json').status(204).end() 187 return res.type('json').status(204).end()
164} 188}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 5ebd8fbc4..a3b1dde29 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -62,6 +62,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
62import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' 62import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' 64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
65import { Hooks } from '../../../lib/plugins/hooks'
65 66
66const auditLogger = auditLoggerFactory('videos') 67const auditLogger = auditLoggerFactory('videos')
67const videosRouter = express.Router() 68const videosRouter = express.Router()
@@ -268,10 +269,7 @@ async function addVideo (req: express.Request, res: express.Response) {
268 } 269 }
269 270
270 const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t) 271 const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
271 272 if (!videoWasAutoBlacklisted) await federateVideoIfNeeded(video, true, t)
272 if (!videoWasAutoBlacklisted) {
273 await federateVideoIfNeeded(video, true, t)
274 }
275 273
276 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 274 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
277 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 275 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
@@ -279,11 +277,8 @@ async function addVideo (req: express.Request, res: express.Response) {
279 return { videoCreated, videoWasAutoBlacklisted } 277 return { videoCreated, videoWasAutoBlacklisted }
280 }) 278 })
281 279
282 if (videoWasAutoBlacklisted) { 280 if (videoWasAutoBlacklisted) Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
283 Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated) 281 else Notifier.Instance.notifyOnNewVideo(videoCreated)
284 } else {
285 Notifier.Instance.notifyOnNewVideo(videoCreated)
286 }
287 282
288 if (video.state === VideoState.TO_TRANSCODE) { 283 if (video.state === VideoState.TO_TRANSCODE) {
289 // Put uuid because we don't have id auto incremented for now 284 // Put uuid because we don't have id auto incremented for now
@@ -307,6 +302,8 @@ async function addVideo (req: express.Request, res: express.Response) {
307 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 302 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
308 } 303 }
309 304
305 Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
306
310 return res.json({ 307 return res.json({
311 video: { 308 video: {
312 id: videoCreated.id, 309 id: videoCreated.id,
@@ -421,6 +418,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
421 if (wasUnlistedVideo || wasPrivateVideo) { 418 if (wasUnlistedVideo || wasPrivateVideo) {
422 Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated) 419 Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated)
423 } 420 }
421
422 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated })
424 } catch (err) { 423 } catch (err) {
425 // Force fields we want to update 424 // Force fields we want to update
426 // If the transaction is retried, sequelize will think the object has not changed 425 // If the transaction is retried, sequelize will think the object has not changed
@@ -436,7 +435,11 @@ async function updateVideo (req: express.Request, res: express.Response) {
436async function getVideo (req: express.Request, res: express.Response) { 435async function getVideo (req: express.Request, res: express.Response) {
437 // We need more attributes 436 // We need more attributes
438 const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null 437 const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
439 const video = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId) 438
439 const video = await Hooks.wrapPromise(
440 VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId),
441 'filter:api.video.get.result'
442 )
440 443
441 if (video.isOutdated()) { 444 if (video.isOutdated()) {
442 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) 445 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
@@ -464,6 +467,8 @@ async function viewVideo (req: express.Request, res: express.Response) {
464 const serverActor = await getServerActor() 467 const serverActor = await getServerActor()
465 await sendView(serverActor, videoInstance, undefined) 468 await sendView(serverActor, videoInstance, undefined)
466 469
470 Hooks.runAction('action:api.video.viewed', { video: videoInstance, ip })
471
467 return res.status(204).end() 472 return res.status(204).end()
468} 473}
469 474
@@ -481,7 +486,7 @@ async function getVideoDescription (req: express.Request, res: express.Response)
481} 486}
482 487
483async function listVideos (req: express.Request, res: express.Response) { 488async function listVideos (req: express.Request, res: express.Response) {
484 const resultList = await VideoModel.listForApi({ 489 const apiOptions = await Hooks.wrapObject({
485 start: req.query.start, 490 start: req.query.start,
486 count: req.query.count, 491 count: req.query.count,
487 sort: req.query.sort, 492 sort: req.query.sort,
@@ -495,7 +500,12 @@ async function listVideos (req: express.Request, res: express.Response) {
495 filter: req.query.filter as VideoFilter, 500 filter: req.query.filter as VideoFilter,
496 withFiles: false, 501 withFiles: false,
497 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 502 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
498 }) 503 }, 'filter:api.videos.list.params')
504
505 const resultList = await Hooks.wrapPromise(
506 VideoModel.listForApi(apiOptions),
507 'filter:api.videos.list.result'
508 )
499 509
500 return res.json(getFormattedObjects(resultList.data, resultList.total)) 510 return res.json(getFormattedObjects(resultList.data, resultList.total))
501} 511}
@@ -510,5 +520,7 @@ async function removeVideo (req: express.Request, res: express.Response) {
510 auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON())) 520 auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
511 logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid) 521 logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
512 522
523 Hooks.runAction('action:api.video.deleted', { video: videoInstance })
524
513 return res.type('json').status(204).end() 525 return res.type('json').status(204).end()
514} 526}
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 9771351e1..38b6f63f8 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -141,7 +141,7 @@ function root () {
141 const paths = [ __dirname, '..', '..' ] 141 const paths = [ __dirname, '..', '..' ]
142 142
143 // We are under /dist directory 143 // We are under /dist directory
144 if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) { 144 if (process.mainModule && process.mainModule.filename.endsWith('_mocha') === false) {
145 paths.push('..') 145 paths.push('..')
146 } 146 }
147 147
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index c3fc6b462..2f26ddefd 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -134,7 +134,7 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []):
134 }) 134 })
135 135
136 if (sanitizeAndCheckVideoCommentObject(body) === false) { 136 if (sanitizeAndCheckVideoCommentObject(body) === false) {
137 throw new Error('Remote video comment JSON is not valid :' + JSON.stringify(body)) 137 throw new Error('Remote video comment JSON is not valid:' + JSON.stringify(body))
138 } 138 }
139 139
140 const actorUrl = body.attributedTo 140 const actorUrl = body.attributedTo
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 4f26cb6be..dade6b55f 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -54,6 +54,8 @@ import { ThumbnailModel } from '../../models/video/thumbnail'
54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
55import { join } from 'path' 55import { join } from 'path'
56import { FilteredModelAttributes } from '../../typings/sequelize' 56import { FilteredModelAttributes } from '../../typings/sequelize'
57import { Hooks } from '../plugins/hooks'
58import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
57 59
58async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 60async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
59 // If the video is not private and is published, we federate it 61 // If the video is not private and is published, we federate it
@@ -236,72 +238,74 @@ async function updateVideoFromAP (options: {
236 channel: VideoChannelModel, 238 channel: VideoChannelModel,
237 overrideTo?: string[] 239 overrideTo?: string[]
238}) { 240}) {
241 const { video, videoObject, account, channel, overrideTo } = options
242
239 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 243 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
240 244
241 let videoFieldsSave: any 245 let videoFieldsSave: any
242 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE 246 const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE
243 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED 247 const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED
244 248
245 try { 249 try {
246 let thumbnailModel: ThumbnailModel 250 let thumbnailModel: ThumbnailModel
247 251
248 try { 252 try {
249 thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE) 253 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
250 } catch (err) { 254 } catch (err) {
251 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) 255 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
252 } 256 }
253 257
254 await sequelizeTypescript.transaction(async t => { 258 await sequelizeTypescript.transaction(async t => {
255 const sequelizeOptions = { transaction: t } 259 const sequelizeOptions = { transaction: t }
256 260
257 videoFieldsSave = options.video.toJSON() 261 videoFieldsSave = video.toJSON()
258 262
259 // Check actor has the right to update the video 263 // Check actor has the right to update the video
260 const videoChannel = options.video.VideoChannel 264 const videoChannel = video.VideoChannel
261 if (videoChannel.Account.id !== options.account.id) { 265 if (videoChannel.Account.id !== account.id) {
262 throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) 266 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
263 } 267 }
264 268
265 const to = options.overrideTo ? options.overrideTo : options.videoObject.to 269 const to = overrideTo ? overrideTo : videoObject.to
266 const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to) 270 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
267 options.video.set('name', videoData.name) 271 video.name = videoData.name
268 options.video.set('uuid', videoData.uuid) 272 video.uuid = videoData.uuid
269 options.video.set('url', videoData.url) 273 video.url = videoData.url
270 options.video.set('category', videoData.category) 274 video.category = videoData.category
271 options.video.set('licence', videoData.licence) 275 video.licence = videoData.licence
272 options.video.set('language', videoData.language) 276 video.language = videoData.language
273 options.video.set('description', videoData.description) 277 video.description = videoData.description
274 options.video.set('support', videoData.support) 278 video.support = videoData.support
275 options.video.set('nsfw', videoData.nsfw) 279 video.nsfw = videoData.nsfw
276 options.video.set('commentsEnabled', videoData.commentsEnabled) 280 video.commentsEnabled = videoData.commentsEnabled
277 options.video.set('downloadEnabled', videoData.downloadEnabled) 281 video.downloadEnabled = videoData.downloadEnabled
278 options.video.set('waitTranscoding', videoData.waitTranscoding) 282 video.waitTranscoding = videoData.waitTranscoding
279 options.video.set('state', videoData.state) 283 video.state = videoData.state
280 options.video.set('duration', videoData.duration) 284 video.duration = videoData.duration
281 options.video.set('createdAt', videoData.createdAt) 285 video.createdAt = videoData.createdAt
282 options.video.set('publishedAt', videoData.publishedAt) 286 video.publishedAt = videoData.publishedAt
283 options.video.set('originallyPublishedAt', videoData.originallyPublishedAt) 287 video.originallyPublishedAt = videoData.originallyPublishedAt
284 options.video.set('privacy', videoData.privacy) 288 video.privacy = videoData.privacy
285 options.video.set('channelId', videoData.channelId) 289 video.channelId = videoData.channelId
286 options.video.set('views', videoData.views) 290 video.views = videoData.views
287 291
288 await options.video.save(sequelizeOptions) 292 await video.save(sequelizeOptions)
289 293
290 if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t) 294 if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
291 295
292 // FIXME: use icon URL instead 296 // FIXME: use icon URL instead
293 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename)) 297 const previewUrl = buildRemoteBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.getPreview().filename))
294 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 298 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
295 await options.video.addAndSaveThumbnail(previewModel, t) 299 await video.addAndSaveThumbnail(previewModel, t)
296 300
297 { 301 {
298 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) 302 const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject)
299 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 303 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
300 304
301 // Remove video files that do not exist anymore 305 // Remove video files that do not exist anymore
302 const destroyTasks = options.video.VideoFiles 306 const destroyTasks = video.VideoFiles
303 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f))) 307 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
304 .map(f => f.destroy(sequelizeOptions)) 308 .map(f => f.destroy(sequelizeOptions))
305 await Promise.all(destroyTasks) 309 await Promise.all(destroyTasks)
306 310
307 // Update or add other one 311 // Update or add other one
@@ -310,21 +314,17 @@ async function updateVideoFromAP (options: {
310 .then(([ file ]) => file) 314 .then(([ file ]) => file)
311 }) 315 })
312 316
313 options.video.VideoFiles = await Promise.all(upsertTasks) 317 video.VideoFiles = await Promise.all(upsertTasks)
314 } 318 }
315 319
316 { 320 {
317 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes( 321 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(video, videoObject, video.VideoFiles)
318 options.video,
319 options.videoObject,
320 options.video.VideoFiles
321 )
322 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) 322 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
323 323
324 // Remove video files that do not exist anymore 324 // Remove video files that do not exist anymore
325 const destroyTasks = options.video.VideoStreamingPlaylists 325 const destroyTasks = video.VideoStreamingPlaylists
326 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) 326 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
327 .map(f => f.destroy(sequelizeOptions)) 327 .map(f => f.destroy(sequelizeOptions))
328 await Promise.all(destroyTasks) 328 await Promise.all(destroyTasks)
329 329
330 // Update or add other one 330 // Update or add other one
@@ -333,36 +333,36 @@ async function updateVideoFromAP (options: {
333 .then(([ streamingPlaylist ]) => streamingPlaylist) 333 .then(([ streamingPlaylist ]) => streamingPlaylist)
334 }) 334 })
335 335
336 options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) 336 video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
337 } 337 }
338 338
339 { 339 {
340 // Update Tags 340 // Update Tags
341 const tags = options.videoObject.tag.map(tag => tag.name) 341 const tags = videoObject.tag.map(tag => tag.name)
342 const tagInstances = await TagModel.findOrCreateTags(tags, t) 342 const tagInstances = await TagModel.findOrCreateTags(tags, t)
343 await options.video.$set('Tags', tagInstances, sequelizeOptions) 343 await video.$set('Tags', tagInstances, sequelizeOptions)
344 } 344 }
345 345
346 { 346 {
347 // Update captions 347 // Update captions
348 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t) 348 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
349 349
350 const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => { 350 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
351 return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t) 351 return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
352 }) 352 })
353 options.video.VideoCaptions = await Promise.all(videoCaptionsPromises) 353 video.VideoCaptions = await Promise.all(videoCaptionsPromises)
354 } 354 }
355 }) 355 })
356 356
357 // Notify our users? 357 const autoBlacklisted = await autoBlacklistVideoIfNeeded(video, undefined, undefined)
358 if (wasPrivateVideo || wasUnlistedVideo) { 358
359 Notifier.Instance.notifyOnNewVideo(options.video) 359 if (autoBlacklisted) Notifier.Instance.notifyOnVideoAutoBlacklist(video)
360 } 360 else if (!wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideo(video) // Notify our users?
361 361
362 logger.info('Remote video with uuid %s updated', options.videoObject.uuid) 362 logger.info('Remote video with uuid %s updated', videoObject.uuid)
363 } catch (err) { 363 } catch (err) {
364 if (options.video !== undefined && videoFieldsSave !== undefined) { 364 if (video !== undefined && videoFieldsSave !== undefined) {
365 resetSequelizeInstance(options.video, videoFieldsSave) 365 resetSequelizeInstance(video, videoFieldsSave)
366 } 366 }
367 367
368 // This is just a debug because we will retry the insert 368 // This is just a debug because we will retry the insert
@@ -379,7 +379,9 @@ async function refreshVideoIfNeeded (options: {
379 if (!options.video.isOutdated()) return options.video 379 if (!options.video.isOutdated()) return options.video
380 380
381 // We need more attributes if the argument video was fetched with not enough joints 381 // We need more attributes if the argument video was fetched with not enough joints
382 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 382 const video = options.fetchedType === 'all'
383 ? options.video
384 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
383 385
384 try { 386 try {
385 const { response, videoObject } = await fetchRemoteVideo(video.url) 387 const { response, videoObject } = await fetchRemoteVideo(video.url)
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
new file mode 100644
index 000000000..b609f4585
--- /dev/null
+++ b/server/lib/moderation.ts
@@ -0,0 +1,64 @@
1import { VideoModel } from '../models/video/video'
2import { VideoCommentModel } from '../models/video/video-comment'
3import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
4import { VideoCreate } from '../../shared/models/videos'
5import { UserModel } from '../models/account/user'
6import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
7import { ActivityCreate } from '../../shared/models/activitypub'
8import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
10
11export type AcceptResult = {
12 accepted: boolean
13 errorMessage?: string
14}
15
16// Can be filtered by plugins
17function isLocalVideoAccepted (object: {
18 videoBody: VideoCreate,
19 videoFile: Express.Multer.File & { duration?: number },
20 user: UserModel
21}): AcceptResult {
22 return { accepted: true }
23}
24
25function isLocalVideoThreadAccepted (_object: {
26 commentBody: VideoCommentCreate,
27 video: VideoModel,
28 user: UserModel
29}): AcceptResult {
30 return { accepted: true }
31}
32
33function isLocalVideoCommentReplyAccepted (_object: {
34 commentBody: VideoCommentCreate,
35 parentComment: VideoCommentModel,
36 video: VideoModel,
37 user: UserModel
38}): AcceptResult {
39 return { accepted: true }
40}
41
42function isRemoteVideoAccepted (_object: {
43 activity: ActivityCreate,
44 videoAP: VideoTorrentObject,
45 byActor: ActorModel
46}): AcceptResult {
47 return { accepted: true }
48}
49
50function isRemoteVideoCommentAccepted (_object: {
51 activity: ActivityCreate,
52 commentAP: VideoCommentObject,
53 byActor: ActorModel
54}): AcceptResult {
55 return { accepted: true }
56}
57
58export {
59 isLocalVideoAccepted,
60 isLocalVideoThreadAccepted,
61 isRemoteVideoAccepted,
62 isRemoteVideoCommentAccepted,
63 isLocalVideoCommentReplyAccepted
64}
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts
new file mode 100644
index 000000000..7bb907e6a
--- /dev/null
+++ b/server/lib/plugins/hooks.ts
@@ -0,0 +1,26 @@
1import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models/plugins/server-hook.model'
2import { PluginManager } from './plugin-manager'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird'
5
6// Helpers to run hooks
7const Hooks = {
8 wrapObject: <T, U extends ServerFilterHookName>(obj: T, hookName: U) => {
9 return PluginManager.Instance.runHook(hookName, obj) as Promise<T>
10 },
11
12 wrapPromise: async <T, U extends ServerFilterHookName>(fun: Promise<T> | Bluebird<T>, hookName: U) => {
13 const result = await fun
14
15 return PluginManager.Instance.runHook(hookName, result)
16 },
17
18 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
19 PluginManager.Instance.runHook(hookName, params)
20 .catch(err => logger.error('Fatal hook error.', { err }))
21 }
22}
23
24export {
25 Hooks
26}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 570b56193..85ee3decb 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -14,6 +14,10 @@ import { RegisterSettingOptions } from '../../../shared/models/plugins/register-
14import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model' 14import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
15import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' 15import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
16import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model' 16import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
17import { ServerHookName, ServerHook } from '../../../shared/models/plugins/server-hook.model'
18import { isCatchable, isPromise } from '../../../shared/core-utils/miscs/miscs'
19import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
20import { HookType } from '../../../shared/models/plugins/hook-type.enum'
17 21
18export interface RegisteredPlugin { 22export interface RegisteredPlugin {
19 npmName: string 23 npmName: string
@@ -42,7 +46,7 @@ export interface HookInformationValue {
42 priority: number 46 priority: number
43} 47}
44 48
45export class PluginManager { 49export class PluginManager implements ServerHook {
46 50
47 private static instance: PluginManager 51 private static instance: PluginManager
48 52
@@ -95,25 +99,17 @@ export class PluginManager {
95 99
96 // ###################### Hooks ###################### 100 // ###################### Hooks ######################
97 101
98 async runHook (hookName: string, param?: any) { 102 async runHook (hookName: ServerHookName, param?: any) {
99 let result = param 103 let result = param
100 104
101 if (!this.hooks[hookName]) return result 105 if (!this.hooks[hookName]) return result
102 106
103 const wait = hookName.startsWith('static:') 107 const hookType = getHookType(hookName)
104 108
105 for (const hook of this.hooks[hookName]) { 109 for (const hook of this.hooks[hookName]) {
106 try { 110 result = await internalRunHook(hook.handler, hookType, param, err => {
107 const p = hook.handler(param)
108
109 if (wait) {
110 result = await p
111 } else if (p.catch) {
112 p.catch(err => logger.warn('Hook %s of plugin %s thrown an error.', hookName, hook.pluginName, { err }))
113 }
114 } catch (err) {
115 logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err }) 111 logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
116 } 112 })
117 } 113 }
118 114
119 return result 115 return result
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index 985b89e31..32b1a28fa 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -1,4 +1,4 @@
1import * as sequelize from 'sequelize' 1import { Transaction } from 'sequelize'
2import { CONFIG } from '../initializers/config' 2import { CONFIG } from '../initializers/config'
3import { UserRight, VideoBlacklistType } from '../../shared/models' 3import { UserRight, VideoBlacklistType } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist' 4import { VideoBlacklistModel } from '../models/video/video-blacklist'
@@ -6,26 +6,39 @@ import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
8import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 8import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
9import { Hooks } from './plugins/hooks'
9 10
10async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) { 11async function autoBlacklistVideoIfNeeded (video: VideoModel, user?: UserModel, transaction?: Transaction) {
11 if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false 12 const doAutoBlacklist = await Hooks.wrapPromise(
13 autoBlacklistNeeded({ video, user }),
14 'filter:video.auto-blacklist.result'
15 )
12 16
13 if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) || user.hasAdminFlag(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)) return false 17 if (!doAutoBlacklist) return false
14 18
15 const sequelizeOptions = { transaction }
16 const videoBlacklistToCreate = { 19 const videoBlacklistToCreate = {
17 videoId: video.id, 20 videoId: video.id,
18 unfederated: true, 21 unfederated: true,
19 reason: 'Auto-blacklisted. Moderator review required.', 22 reason: 'Auto-blacklisted. Moderator review required.',
20 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 23 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
21 } 24 }
22 await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions) 25 await VideoBlacklistModel.create(videoBlacklistToCreate, { transaction })
23 26
24 logger.info('Video %s auto-blacklisted.', video.uuid) 27 logger.info('Video %s auto-blacklisted.', video.uuid)
25 28
26 return true 29 return true
27} 30}
28 31
32async function autoBlacklistNeeded (parameters: { video: VideoModel, user?: UserModel }) {
33 const { user } = parameters
34
35 if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED || !user) return false
36
37 if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) || user.hasAdminFlag(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)) return false
38
39 return true
40}
41
29// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
30 43
31export { 44export {
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index ffde208b7..9c1bfaeaa 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -9,6 +9,8 @@ import { UserModel } from '../../../models/account/user'
9import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
10import { VideoCommentModel } from '../../../models/video/video-comment' 10import { VideoCommentModel } from '../../../models/video/video-comment'
11import { areValidationErrors } from '../utils' 11import { areValidationErrors } from '../utils'
12import { Hooks } from '../../../lib/plugins/hooks'
13import { isLocalVideoThreadAccepted, isLocalVideoCommentReplyAccepted, AcceptResult } from '../../../lib/moderation'
12 14
13const listVideoCommentThreadsValidator = [ 15const listVideoCommentThreadsValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -48,6 +50,7 @@ const addVideoCommentThreadValidator = [
48 if (areValidationErrors(req, res)) return 50 if (areValidationErrors(req, res)) return
49 if (!await doesVideoExist(req.params.videoId, res)) return 51 if (!await doesVideoExist(req.params.videoId, res)) return
50 if (!isVideoCommentsEnabled(res.locals.video, res)) return 52 if (!isVideoCommentsEnabled(res.locals.video, res)) return
53 if (!await isVideoCommentAccepted(req, res, false)) return
51 54
52 return next() 55 return next()
53 } 56 }
@@ -65,6 +68,7 @@ const addVideoCommentReplyValidator = [
65 if (!await doesVideoExist(req.params.videoId, res)) return 68 if (!await doesVideoExist(req.params.videoId, res)) return
66 if (!isVideoCommentsEnabled(res.locals.video, res)) return 69 if (!isVideoCommentsEnabled(res.locals.video, res)) return
67 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return 70 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
71 if (!await isVideoCommentAccepted(req, res, true)) return
68 72
69 return next() 73 return next()
70 } 74 }
@@ -193,3 +197,37 @@ function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCom
193 197
194 return true 198 return true
195} 199}
200
201async function isVideoCommentAccepted (req: express.Request, res: express.Response, isReply: boolean) {
202 const acceptParameters = {
203 video: res.locals.video,
204 commentBody: req.body,
205 user: res.locals.oauth.token.User
206 }
207
208 let acceptedResult: AcceptResult
209
210 if (isReply) {
211 const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoComment })
212
213 acceptedResult = await Hooks.wrapObject(
214 isLocalVideoCommentReplyAccepted(acceptReplyParameters),
215 'filter:api.video-comment-reply.create.accept.result'
216 )
217 } else {
218 acceptedResult = await Hooks.wrapObject(
219 isLocalVideoThreadAccepted(acceptParameters),
220 'filter:api.video-thread.create.accept.result'
221 )
222 }
223
224 if (!acceptedResult || acceptedResult.accepted !== true) {
225 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
226 res.status(403)
227 .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
228
229 return false
230 }
231
232 return true
233}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index b1c05ab2d..cb2c071ba 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -33,7 +33,7 @@ import {
33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34import { logger } from '../../../helpers/logger' 34import { logger } from '../../../helpers/logger'
35import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 35import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
36import { authenticatePromiseIfNeeded } from '../../oauth' 36import { authenticate, authenticatePromiseIfNeeded } from '../../oauth'
37import { areValidationErrors } from '../utils' 37import { areValidationErrors } from '../utils'
38import { cleanUpReqFiles } from '../../../helpers/express-utils' 38import { cleanUpReqFiles } from '../../../helpers/express-utils'
39import { VideoModel } from '../../../models/video/video' 39import { VideoModel } from '../../../models/video/video'
@@ -44,6 +44,8 @@ import { VideoFetchType } from '../../../helpers/video'
44import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 44import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45import { getServerActor } from '../../../helpers/utils' 45import { getServerActor } from '../../../helpers/utils'
46import { CONFIG } from '../../../initializers/config' 46import { CONFIG } from '../../../initializers/config'
47import { isLocalVideoAccepted } from '../../../lib/moderation'
48import { Hooks } from '../../../lib/plugins/hooks'
47 49
48const videosAddValidator = getCommonVideoEditAttributes().concat([ 50const videosAddValidator = getCommonVideoEditAttributes().concat([
49 body('videofile') 51 body('videofile')
@@ -62,14 +64,12 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
62 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 64 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
63 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) 65 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
64 66
65 const videoFile: Express.Multer.File = req.files['videofile'][0] 67 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User 68 const user = res.locals.oauth.token.User
67 69
68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 70 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
69 71
70 const isAble = await user.isAbleToUploadVideo(videoFile) 72 if (await user.isAbleToUploadVideo(videoFile) === false) {
71
72 if (isAble === false) {
73 res.status(403) 73 res.status(403)
74 .json({ error: 'The user video quota is exceeded with this video.' }) 74 .json({ error: 'The user video quota is exceeded with this video.' })
75 75
@@ -88,7 +88,9 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
88 return cleanUpReqFiles(req) 88 return cleanUpReqFiles(req)
89 } 89 }
90 90
91 videoFile['duration'] = duration 91 videoFile.duration = duration
92
93 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
92 94
93 return next() 95 return next()
94 } 96 }
@@ -434,3 +436,26 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
434 436
435 return false 437 return false
436} 438}
439
440async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
441 // Check we accept this video
442 const acceptParameters = {
443 videoBody: req.body,
444 videoFile,
445 user: res.locals.oauth.token.User
446 }
447 const acceptedResult = await Hooks.wrapObject(
448 isLocalVideoAccepted(acceptParameters),
449 'filter:api.video.upload.accept.result'
450 )
451
452 if (!acceptedResult || acceptedResult.accepted !== true) {
453 logger.info('Refused local video.', { acceptedResult, acceptParameters })
454 res.status(403)
455 .json({ error: acceptedResult.errorMessage || 'Refused local video' })
456
457 return false
458 }
459
460 return true
461}
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index fee11ec5f..536b6cb3e 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -293,7 +293,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
293 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) 293 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
294 } 294 }
295 295
296 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) { 296 static async listThreadsForApi (parameters: {
297 videoId: number,
298 start: number,
299 count: number,
300 sort: string,
301 user?: UserModel
302 }) {
303 const { videoId, start, count, sort, user } = parameters
304
297 const serverActor = await getServerActor() 305 const serverActor = await getServerActor()
298 const serverAccountId = serverActor.Account.id 306 const serverAccountId = serverActor.Account.id
299 const userAccountId = user ? user.Account.id : undefined 307 const userAccountId = user ? user.Account.id : undefined
@@ -328,7 +336,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
328 }) 336 })
329 } 337 }
330 338
331 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) { 339 static async listThreadCommentsForApi (parameters: {
340 videoId: number,
341 threadId: number,
342 user?: UserModel
343 }) {
344 const { videoId, threadId, user } = parameters
345
332 const serverActor = await getServerActor() 346 const serverActor = await getServerActor()
333 const serverAccountId = serverActor.Account.id 347 const serverAccountId = serverActor.Account.id
334 const userAccountId = user ? user.Account.id : undefined 348 const userAccountId = user ? user.Account.id : undefined