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()
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,
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,
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()
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()
}
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()
}
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()
}
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)
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
await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
}
+ Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
+
return res.json({
video: {
id: videoCreated.id,
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
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 } })
const serverActor = await getServerActor()
await sendView(serverActor, videoInstance, undefined)
+ Hooks.runAction('action:api.video.viewed', { video: videoInstance, ip })
+
return res.status(204).end()
}
}
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,
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))
}
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()
}
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('..')
}
})
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
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
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)
-
- await options.video.save(sequelizeOptions)
-
- if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
+ 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 video.save(sequelizeOptions)
+
+ 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
.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
.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)
+
+ 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', options.videoObject.uuid)
+ 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
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)
--- /dev/null
+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
+}
--- /dev/null
+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
+}
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
priority: number
}
-export class PluginManager {
+export class PluginManager implements ServerHook {
private static instance: 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
-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'
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 {
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'),
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()
}
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()
}
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
+}
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'
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')
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.' })
return cleanUpReqFiles(req)
}
- videoFile['duration'] = duration
+ videoFile.duration = duration
+
+ if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
return next()
}
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
+}
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
})
}
- 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
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
}
--- /dev/null
+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
+}
--- /dev/null
+export enum HookType {
+ STATIC = 1,
+ ACTION = 2,
+ FILTER = 3
+}
--- /dev/null
+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)
+}
"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