Add server hooks

This commit is contained in:
Chocobozzz 2019-07-18 14:28:37 +02:00 committed by Chocobozzz
parent 66e001c848
commit b4055e1c23
17 changed files with 422 additions and 112 deletions

View file

@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment'
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
import { AccountModel } from '../../../models/account/account'
import { Notifier } from '../../../lib/notifier'
import { Hooks } from '../../../lib/plugins/hooks'
const auditLogger = auditLoggerFactory('comments')
const videoCommentRouter = express.Router()
@ -76,7 +77,18 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
let resultList: ResultList<VideoCommentModel>
if (video.commentsEnabled === true) {
resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user)
const apiOptions = await Hooks.wrapObject({
videoId: video.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
user: user
}, 'filter:api.video-threads.list.params')
resultList = await Hooks.wrapPromise(
VideoCommentModel.listThreadsForApi(apiOptions),
'filter:api.video-threads.list.result'
)
} else {
resultList = {
total: 0,
@ -94,7 +106,16 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
let resultList: ResultList<VideoCommentModel>
if (video.commentsEnabled === true) {
resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user)
const apiOptions = await Hooks.wrapObject({
videoId: video.id,
threadId: res.locals.videoCommentThread.id,
user: user
}, 'filter:api.video-thread-comments.list.params')
resultList = await Hooks.wrapPromise(
VideoCommentModel.listThreadCommentsForApi(apiOptions),
'filter:api.video-thread-comments.list.result'
)
} else {
resultList = {
total: 0,
@ -122,6 +143,8 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
Notifier.Instance.notifyOnNewComment(comment)
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
Hooks.runAction('action:api.video-thread.created', { comment })
return res.json({
comment: comment.toFormattedJSON()
}).end()
@ -144,6 +167,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
Notifier.Instance.notifyOnNewComment(comment)
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
Hooks.runAction('action:api.video-comment-reply.created', { comment })
return res.json({ comment: comment.toFormattedJSON() }).end()
}
@ -154,11 +179,10 @@ async function removeVideoComment (req: express.Request, res: express.Response)
await videoCommentInstance.destroy({ transaction: t })
})
auditLogger.delete(
getAuditIdFromRes(res),
new CommentAuditView(videoCommentInstance.toFormattedJSON())
)
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
logger.info('Video comment %d deleted.', videoCommentInstance.id)
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstance })
return res.type('json').status(204).end()
}

View file

@ -62,6 +62,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
import { Hooks } from '../../../lib/plugins/hooks'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
@ -268,10 +269,7 @@ async function addVideo (req: express.Request, res: express.Response) {
}
const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
if (!videoWasAutoBlacklisted) {
await federateVideoIfNeeded(video, true, t)
}
if (!videoWasAutoBlacklisted) await federateVideoIfNeeded(video, true, t)
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
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) {
return { videoCreated, videoWasAutoBlacklisted }
})
if (videoWasAutoBlacklisted) {
Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
} else {
Notifier.Instance.notifyOnNewVideo(videoCreated)
}
if (videoWasAutoBlacklisted) Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
else Notifier.Instance.notifyOnNewVideo(videoCreated)
if (video.state === VideoState.TO_TRANSCODE) {
// 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) {
await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
}
Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
return res.json({
video: {
id: videoCreated.id,
@ -421,6 +418,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
if (wasUnlistedVideo || wasPrivateVideo) {
Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated)
}
Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated })
} catch (err) {
// Force fields we want to update
// 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) {
async function getVideo (req: express.Request, res: express.Response) {
// We need more attributes
const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
const video = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId)
const video = await Hooks.wrapPromise(
VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId),
'filter:api.video.get.result'
)
if (video.isOutdated()) {
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) {
const serverActor = await getServerActor()
await sendView(serverActor, videoInstance, undefined)
Hooks.runAction('action:api.video.viewed', { video: videoInstance, ip })
return res.status(204).end()
}
@ -481,7 +486,7 @@ async function getVideoDescription (req: express.Request, res: express.Response)
}
async function listVideos (req: express.Request, res: express.Response) {
const resultList = await VideoModel.listForApi({
const apiOptions = await Hooks.wrapObject({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
@ -495,7 +500,12 @@ async function listVideos (req: express.Request, res: express.Response) {
filter: req.query.filter as VideoFilter,
withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
})
}, 'filter:api.videos.list.params')
const resultList = await Hooks.wrapPromise(
VideoModel.listForApi(apiOptions),
'filter:api.videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
@ -510,5 +520,7 @@ async function removeVideo (req: express.Request, res: express.Response) {
auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
Hooks.runAction('action:api.video.deleted', { video: videoInstance })
return res.type('json').status(204).end()
}

View file

@ -141,7 +141,7 @@ function root () {
const paths = [ __dirname, '..', '..' ]
// We are under /dist directory
if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
if (process.mainModule && process.mainModule.filename.endsWith('_mocha') === false) {
paths.push('..')
}

View file

@ -134,7 +134,7 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []):
})
if (sanitizeAndCheckVideoCommentObject(body) === false) {
throw new Error('Remote video comment JSON is not valid :' + JSON.stringify(body))
throw new Error('Remote video comment JSON is not valid:' + JSON.stringify(body))
}
const actorUrl = body.attributedTo

View file

@ -54,6 +54,8 @@ import { ThumbnailModel } from '../../models/video/thumbnail'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { join } from 'path'
import { FilteredModelAttributes } from '../../typings/sequelize'
import { Hooks } from '../plugins/hooks'
import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
// If the video is not private and is published, we federate it
@ -236,72 +238,74 @@ async function updateVideoFromAP (options: {
channel: VideoChannelModel,
overrideTo?: string[]
}) {
const { video, videoObject, account, channel, overrideTo } = options
logger.debug('Updating remote video "%s".', options.videoObject.uuid)
let videoFieldsSave: any
const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE
const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED
try {
let thumbnailModel: ThumbnailModel
try {
thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
} catch (err) {
logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
}
await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
videoFieldsSave = options.video.toJSON()
videoFieldsSave = video.toJSON()
// Check actor has the right to update the video
const videoChannel = options.video.VideoChannel
if (videoChannel.Account.id !== options.account.id) {
throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
const videoChannel = video.VideoChannel
if (videoChannel.Account.id !== account.id) {
throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
}
const to = options.overrideTo ? options.overrideTo : options.videoObject.to
const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
options.video.set('name', videoData.name)
options.video.set('uuid', videoData.uuid)
options.video.set('url', videoData.url)
options.video.set('category', videoData.category)
options.video.set('licence', videoData.licence)
options.video.set('language', videoData.language)
options.video.set('description', videoData.description)
options.video.set('support', videoData.support)
options.video.set('nsfw', videoData.nsfw)
options.video.set('commentsEnabled', videoData.commentsEnabled)
options.video.set('downloadEnabled', videoData.downloadEnabled)
options.video.set('waitTranscoding', videoData.waitTranscoding)
options.video.set('state', videoData.state)
options.video.set('duration', videoData.duration)
options.video.set('createdAt', videoData.createdAt)
options.video.set('publishedAt', videoData.publishedAt)
options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
options.video.set('privacy', videoData.privacy)
options.video.set('channelId', videoData.channelId)
options.video.set('views', videoData.views)
const to = overrideTo ? overrideTo : videoObject.to
const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
video.name = videoData.name
video.uuid = videoData.uuid
video.url = videoData.url
video.category = videoData.category
video.licence = videoData.licence
video.language = videoData.language
video.description = videoData.description
video.support = videoData.support
video.nsfw = videoData.nsfw
video.commentsEnabled = videoData.commentsEnabled
video.downloadEnabled = videoData.downloadEnabled
video.waitTranscoding = videoData.waitTranscoding
video.state = videoData.state
video.duration = videoData.duration
video.createdAt = videoData.createdAt
video.publishedAt = videoData.publishedAt
video.originallyPublishedAt = videoData.originallyPublishedAt
video.privacy = videoData.privacy
video.channelId = videoData.channelId
video.views = videoData.views
await options.video.save(sequelizeOptions)
await video.save(sequelizeOptions)
if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
// FIXME: use icon URL instead
const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
await options.video.addAndSaveThumbnail(previewModel, t)
const previewUrl = buildRemoteBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.getPreview().filename))
const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
await video.addAndSaveThumbnail(previewModel, t)
{
const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject)
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
// Remove video files that do not exist anymore
const destroyTasks = options.video.VideoFiles
.filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
.map(f => f.destroy(sequelizeOptions))
const destroyTasks = video.VideoFiles
.filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
.map(f => f.destroy(sequelizeOptions))
await Promise.all(destroyTasks)
// Update or add other one
@ -310,21 +314,17 @@ async function updateVideoFromAP (options: {
.then(([ file ]) => file)
})
options.video.VideoFiles = await Promise.all(upsertTasks)
video.VideoFiles = await Promise.all(upsertTasks)
}
{
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(
options.video,
options.videoObject,
options.video.VideoFiles
)
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(video, videoObject, video.VideoFiles)
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
// Remove video files that do not exist anymore
const destroyTasks = options.video.VideoStreamingPlaylists
.filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
.map(f => f.destroy(sequelizeOptions))
const destroyTasks = video.VideoStreamingPlaylists
.filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
.map(f => f.destroy(sequelizeOptions))
await Promise.all(destroyTasks)
// Update or add other one
@ -333,36 +333,36 @@ async function updateVideoFromAP (options: {
.then(([ streamingPlaylist ]) => streamingPlaylist)
})
options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
}
{
// Update Tags
const tags = options.videoObject.tag.map(tag => tag.name)
const tags = videoObject.tag.map(tag => tag.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await options.video.$set('Tags', tagInstances, sequelizeOptions)
await video.$set('Tags', tagInstances, sequelizeOptions)
}
{
// Update captions
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => {
return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t)
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
})
options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
video.VideoCaptions = await Promise.all(videoCaptionsPromises)
}
})
// Notify our users?
if (wasPrivateVideo || wasUnlistedVideo) {
Notifier.Instance.notifyOnNewVideo(options.video)
}
const autoBlacklisted = await autoBlacklistVideoIfNeeded(video, undefined, undefined)
logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
if (autoBlacklisted) Notifier.Instance.notifyOnVideoAutoBlacklist(video)
else if (!wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideo(video) // Notify our users?
logger.info('Remote video with uuid %s updated', videoObject.uuid)
} catch (err) {
if (options.video !== undefined && videoFieldsSave !== undefined) {
resetSequelizeInstance(options.video, videoFieldsSave)
if (video !== undefined && videoFieldsSave !== undefined) {
resetSequelizeInstance(video, videoFieldsSave)
}
// This is just a debug because we will retry the insert
@ -379,7 +379,9 @@ async function refreshVideoIfNeeded (options: {
if (!options.video.isOutdated()) return options.video
// We need more attributes if the argument video was fetched with not enough joints
const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
const video = options.fetchedType === 'all'
? options.video
: await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
try {
const { response, videoObject } = await fetchRemoteVideo(video.url)

64
server/lib/moderation.ts Normal file
View file

@ -0,0 +1,64 @@
import { VideoModel } from '../models/video/video'
import { VideoCommentModel } from '../models/video/video-comment'
import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
import { VideoCreate } from '../../shared/models/videos'
import { UserModel } from '../models/account/user'
import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
import { ActivityCreate } from '../../shared/models/activitypub'
import { ActorModel } from '../models/activitypub/actor'
import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
export type AcceptResult = {
accepted: boolean
errorMessage?: string
}
// Can be filtered by plugins
function isLocalVideoAccepted (object: {
videoBody: VideoCreate,
videoFile: Express.Multer.File & { duration?: number },
user: UserModel
}): AcceptResult {
return { accepted: true }
}
function isLocalVideoThreadAccepted (_object: {
commentBody: VideoCommentCreate,
video: VideoModel,
user: UserModel
}): AcceptResult {
return { accepted: true }
}
function isLocalVideoCommentReplyAccepted (_object: {
commentBody: VideoCommentCreate,
parentComment: VideoCommentModel,
video: VideoModel,
user: UserModel
}): AcceptResult {
return { accepted: true }
}
function isRemoteVideoAccepted (_object: {
activity: ActivityCreate,
videoAP: VideoTorrentObject,
byActor: ActorModel
}): AcceptResult {
return { accepted: true }
}
function isRemoteVideoCommentAccepted (_object: {
activity: ActivityCreate,
commentAP: VideoCommentObject,
byActor: ActorModel
}): AcceptResult {
return { accepted: true }
}
export {
isLocalVideoAccepted,
isLocalVideoThreadAccepted,
isRemoteVideoAccepted,
isRemoteVideoCommentAccepted,
isLocalVideoCommentReplyAccepted
}

View file

@ -0,0 +1,26 @@
import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models/plugins/server-hook.model'
import { PluginManager } from './plugin-manager'
import { logger } from '../../helpers/logger'
import * as Bluebird from 'bluebird'
// Helpers to run hooks
const Hooks = {
wrapObject: <T, U extends ServerFilterHookName>(obj: T, hookName: U) => {
return PluginManager.Instance.runHook(hookName, obj) as Promise<T>
},
wrapPromise: async <T, U extends ServerFilterHookName>(fun: Promise<T> | Bluebird<T>, hookName: U) => {
const result = await fun
return PluginManager.Instance.runHook(hookName, result)
},
runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
PluginManager.Instance.runHook(hookName, params)
.catch(err => logger.error('Fatal hook error.', { err }))
}
}
export {
Hooks
}

View file

@ -14,6 +14,10 @@ import { RegisterSettingOptions } from '../../../shared/models/plugins/register-
import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
import { ServerHookName, ServerHook } from '../../../shared/models/plugins/server-hook.model'
import { isCatchable, isPromise } from '../../../shared/core-utils/miscs/miscs'
import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
import { HookType } from '../../../shared/models/plugins/hook-type.enum'
export interface RegisteredPlugin {
npmName: string
@ -42,7 +46,7 @@ export interface HookInformationValue {
priority: number
}
export class PluginManager {
export class PluginManager implements ServerHook {
private static instance: PluginManager
@ -95,25 +99,17 @@ export class PluginManager {
// ###################### Hooks ######################
async runHook (hookName: string, param?: any) {
async runHook (hookName: ServerHookName, param?: any) {
let result = param
if (!this.hooks[hookName]) return result
const wait = hookName.startsWith('static:')
const hookType = getHookType(hookName)
for (const hook of this.hooks[hookName]) {
try {
const p = hook.handler(param)
if (wait) {
result = await p
} else if (p.catch) {
p.catch(err => logger.warn('Hook %s of plugin %s thrown an error.', hookName, hook.pluginName, { err }))
}
} catch (err) {
result = await internalRunHook(hook.handler, hookType, param, err => {
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
}
})
}
return result

View file

@ -1,4 +1,4 @@
import * as sequelize from 'sequelize'
import { Transaction } from 'sequelize'
import { CONFIG } from '../initializers/config'
import { UserRight, VideoBlacklistType } from '../../shared/models'
import { VideoBlacklistModel } from '../models/video/video-blacklist'
@ -6,26 +6,39 @@ import { UserModel } from '../models/account/user'
import { VideoModel } from '../models/video/video'
import { logger } from '../helpers/logger'
import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
import { Hooks } from './plugins/hooks'
async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
async function autoBlacklistVideoIfNeeded (video: VideoModel, user?: UserModel, transaction?: Transaction) {
const doAutoBlacklist = await Hooks.wrapPromise(
autoBlacklistNeeded({ video, user }),
'filter:video.auto-blacklist.result'
)
if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) || user.hasAdminFlag(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)) return false
if (!doAutoBlacklist) return false
const sequelizeOptions = { transaction }
const videoBlacklistToCreate = {
videoId: video.id,
unfederated: true,
reason: 'Auto-blacklisted. Moderator review required.',
type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
}
await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
await VideoBlacklistModel.create(videoBlacklistToCreate, { transaction })
logger.info('Video %s auto-blacklisted.', video.uuid)
return true
}
async function autoBlacklistNeeded (parameters: { video: VideoModel, user?: UserModel }) {
const { user } = parameters
if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED || !user) return false
if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) || user.hasAdminFlag(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)) return false
return true
}
// ---------------------------------------------------------------------------
export {

View file

@ -9,6 +9,8 @@ import { UserModel } from '../../../models/account/user'
import { VideoModel } from '../../../models/video/video'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { areValidationErrors } from '../utils'
import { Hooks } from '../../../lib/plugins/hooks'
import { isLocalVideoThreadAccepted, isLocalVideoCommentReplyAccepted, AcceptResult } from '../../../lib/moderation'
const listVideoCommentThreadsValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@ -48,6 +50,7 @@ const addVideoCommentThreadValidator = [
if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res)) return
if (!isVideoCommentsEnabled(res.locals.video, res)) return
if (!await isVideoCommentAccepted(req, res, false)) return
return next()
}
@ -65,6 +68,7 @@ const addVideoCommentReplyValidator = [
if (!await doesVideoExist(req.params.videoId, res)) return
if (!isVideoCommentsEnabled(res.locals.video, res)) return
if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
if (!await isVideoCommentAccepted(req, res, true)) return
return next()
}
@ -193,3 +197,37 @@ function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCom
return true
}
async function isVideoCommentAccepted (req: express.Request, res: express.Response, isReply: boolean) {
const acceptParameters = {
video: res.locals.video,
commentBody: req.body,
user: res.locals.oauth.token.User
}
let acceptedResult: AcceptResult
if (isReply) {
const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoComment })
acceptedResult = await Hooks.wrapObject(
isLocalVideoCommentReplyAccepted(acceptReplyParameters),
'filter:api.video-comment-reply.create.accept.result'
)
} else {
acceptedResult = await Hooks.wrapObject(
isLocalVideoThreadAccepted(acceptParameters),
'filter:api.video-thread.create.accept.result'
)
}
if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local comment.', { acceptedResult, acceptParameters })
res.status(403)
.json({ error: acceptedResult.errorMessage || 'Refused local comment' })
return false
}
return true
}

View file

@ -33,7 +33,7 @@ import {
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
import { authenticatePromiseIfNeeded } from '../../oauth'
import { authenticate, authenticatePromiseIfNeeded } from '../../oauth'
import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
import { VideoModel } from '../../../models/video/video'
@ -44,6 +44,8 @@ import { VideoFetchType } from '../../../helpers/video'
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
import { getServerActor } from '../../../helpers/utils'
import { CONFIG } from '../../../initializers/config'
import { isLocalVideoAccepted } from '../../../lib/moderation'
import { Hooks } from '../../../lib/plugins/hooks'
const videosAddValidator = getCommonVideoEditAttributes().concat([
body('videofile')
@ -62,14 +64,12 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
const videoFile: Express.Multer.File = req.files['videofile'][0]
const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
const user = res.locals.oauth.token.User
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
const isAble = await user.isAbleToUploadVideo(videoFile)
if (isAble === false) {
if (await user.isAbleToUploadVideo(videoFile) === false) {
res.status(403)
.json({ error: 'The user video quota is exceeded with this video.' })
@ -88,7 +88,9 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
return cleanUpReqFiles(req)
}
videoFile['duration'] = duration
videoFile.duration = duration
if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
return next()
}
@ -434,3 +436,26 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
return false
}
async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
// Check we accept this video
const acceptParameters = {
videoBody: req.body,
videoFile,
user: res.locals.oauth.token.User
}
const acceptedResult = await Hooks.wrapObject(
isLocalVideoAccepted(acceptParameters),
'filter:api.video.upload.accept.result'
)
if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local video.', { acceptedResult, acceptParameters })
res.status(403)
.json({ error: acceptedResult.errorMessage || 'Refused local video' })
return false
}
return true
}

View file

@ -293,7 +293,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
}
static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
static async listThreadsForApi (parameters: {
videoId: number,
start: number,
count: number,
sort: string,
user?: UserModel
}) {
const { videoId, start, count, sort, user } = parameters
const serverActor = await getServerActor()
const serverAccountId = serverActor.Account.id
const userAccountId = user ? user.Account.id : undefined
@ -328,7 +336,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
})
}
static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
static async listThreadCommentsForApi (parameters: {
videoId: number,
threadId: number,
user?: UserModel
}) {
const { videoId, threadId, user } = parameters
const serverActor = await getServerActor()
const serverAccountId = serverActor.Account.id
const userAccountId = user ? user.Account.id : undefined

View file

@ -19,7 +19,17 @@ function compareSemVer (a: string, b: string) {
return segmentsA.length - segmentsB.length
}
function isPromise (value: any) {
return value && typeof value.then === 'function'
}
function isCatchable (value: any) {
return value && typeof value.catch === 'function'
}
export {
randomInt,
compareSemVer
compareSemVer,
isPromise,
isCatchable
}

View file

@ -0,0 +1,41 @@
import { HookType } from '../../models/plugins/hook-type.enum'
import { isCatchable, isPromise } from '../miscs/miscs'
function getHookType (hookName: string) {
if (hookName.startsWith('filter:')) return HookType.FILTER
if (hookName.startsWith('action:')) return HookType.ACTION
return HookType.STATIC
}
async function internalRunHook (handler: Function, hookType: HookType, param: any, onError: (err: Error) => void) {
let result = param
try {
const p = handler(result)
switch (hookType) {
case HookType.FILTER:
if (isPromise(p)) result = await p
else result = p
break
case HookType.STATIC:
if (isPromise(p)) await p
break
case HookType.ACTION:
if (isCatchable(p)) p.catch(err => onError(err))
break
}
} catch (err) {
onError(err)
}
return result
}
export {
getHookType,
internalRunHook
}

View file

@ -0,0 +1,5 @@
export enum HookType {
STATIC = 1,
ACTION = 2,
FILTER = 3
}

View file

@ -0,0 +1,34 @@
export type ServerFilterHookName =
'filter:api.videos.list.params' |
'filter:api.videos.list.result' |
'filter:api.video.get.result' |
'filter:api.video.upload.accept.result' |
'filter:api.video-thread.create.accept.result' |
'filter:api.video-comment-reply.create.accept.result' |
'filter:api.video-thread-comments.list.params' |
'filter:api.video-thread-comments.list.result' |
'filter:api.video-threads.list.params' |
'filter:api.video-threads.list.result' |
'filter:video.auto-blacklist.result'
export type ServerActionHookName =
'action:application.listening' |
'action:api.video.updated' |
'action:api.video.deleted' |
'action:api.video.uploaded' |
'action:api.video.viewed' |
'action:api.video-thread.created' |
'action:api.video-comment-reply.created' |
'action:api.video-comment.deleted'
export type ServerHookName = ServerFilterHookName | ServerActionHookName
export interface ServerHook {
runHook (hookName: ServerHookName, params?: any)
}

View file

@ -5,7 +5,13 @@
"no-inferrable-types": true,
"eofline": true,
"indent": [true, "spaces"],
"ter-indent": [true, 2],
"ter-indent": [
true,
2,
{
"SwitchCase": 1
}
],
"max-line-length": [true, 140],
"no-unused-variable": false, // Memory issues
"no-floating-promises": false