aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-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
6 files changed, 186 insertions, 85 deletions
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 {