Add server hooks
This commit is contained in:
parent
66e001c848
commit
b4055e1c23
|
@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { Notifier } from '../../../lib/notifier'
|
import { Notifier } from '../../../lib/notifier'
|
||||||
|
import { Hooks } from '../../../lib/plugins/hooks'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('comments')
|
const auditLogger = auditLoggerFactory('comments')
|
||||||
const videoCommentRouter = express.Router()
|
const videoCommentRouter = express.Router()
|
||||||
|
@ -76,7 +77,18 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
|
||||||
let resultList: ResultList<VideoCommentModel>
|
let resultList: ResultList<VideoCommentModel>
|
||||||
|
|
||||||
if (video.commentsEnabled === true) {
|
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 {
|
} else {
|
||||||
resultList = {
|
resultList = {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -94,7 +106,16 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
|
||||||
let resultList: ResultList<VideoCommentModel>
|
let resultList: ResultList<VideoCommentModel>
|
||||||
|
|
||||||
if (video.commentsEnabled === true) {
|
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 {
|
} else {
|
||||||
resultList = {
|
resultList = {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -122,6 +143,8 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
|
||||||
Notifier.Instance.notifyOnNewComment(comment)
|
Notifier.Instance.notifyOnNewComment(comment)
|
||||||
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
|
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video-thread.created', { comment })
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
comment: comment.toFormattedJSON()
|
comment: comment.toFormattedJSON()
|
||||||
}).end()
|
}).end()
|
||||||
|
@ -144,6 +167,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
|
||||||
Notifier.Instance.notifyOnNewComment(comment)
|
Notifier.Instance.notifyOnNewComment(comment)
|
||||||
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
|
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video-comment-reply.created', { comment })
|
||||||
|
|
||||||
return res.json({ comment: comment.toFormattedJSON() }).end()
|
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 })
|
await videoCommentInstance.destroy({ transaction: t })
|
||||||
})
|
})
|
||||||
|
|
||||||
auditLogger.delete(
|
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
|
||||||
getAuditIdFromRes(res),
|
|
||||||
new CommentAuditView(videoCommentInstance.toFormattedJSON())
|
|
||||||
)
|
|
||||||
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstance })
|
||||||
|
|
||||||
return res.type('json').status(204).end()
|
return res.type('json').status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
|
import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
|
||||||
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
|
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
|
||||||
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
|
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
|
||||||
|
import { Hooks } from '../../../lib/plugins/hooks'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
const videosRouter = express.Router()
|
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)
|
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()))
|
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
|
||||||
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
|
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 }
|
return { videoCreated, videoWasAutoBlacklisted }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (videoWasAutoBlacklisted) {
|
if (videoWasAutoBlacklisted) Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
|
||||||
Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
|
else Notifier.Instance.notifyOnNewVideo(videoCreated)
|
||||||
} else {
|
|
||||||
Notifier.Instance.notifyOnNewVideo(videoCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video.state === VideoState.TO_TRANSCODE) {
|
if (video.state === VideoState.TO_TRANSCODE) {
|
||||||
// Put uuid because we don't have id auto incremented for now
|
// 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 })
|
await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
video: {
|
video: {
|
||||||
id: videoCreated.id,
|
id: videoCreated.id,
|
||||||
|
@ -421,6 +418,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||||
if (wasUnlistedVideo || wasPrivateVideo) {
|
if (wasUnlistedVideo || wasPrivateVideo) {
|
||||||
Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated)
|
Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Force fields we want to update
|
// Force fields we want to update
|
||||||
// If the transaction is retried, sequelize will think the object has not changed
|
// 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) {
|
async function getVideo (req: express.Request, res: express.Response) {
|
||||||
// We need more attributes
|
// We need more attributes
|
||||||
const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
|
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()) {
|
if (video.isOutdated()) {
|
||||||
JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
|
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()
|
const serverActor = await getServerActor()
|
||||||
await sendView(serverActor, videoInstance, undefined)
|
await sendView(serverActor, videoInstance, undefined)
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video.viewed', { video: videoInstance, ip })
|
||||||
|
|
||||||
return res.status(204).end()
|
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) {
|
async function listVideos (req: express.Request, res: express.Response) {
|
||||||
const resultList = await VideoModel.listForApi({
|
const apiOptions = await Hooks.wrapObject({
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
|
@ -495,7 +500,12 @@ async function listVideos (req: express.Request, res: express.Response) {
|
||||||
filter: req.query.filter as VideoFilter,
|
filter: req.query.filter as VideoFilter,
|
||||||
withFiles: false,
|
withFiles: false,
|
||||||
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
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))
|
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()))
|
auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
|
||||||
logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
|
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()
|
return res.type('json').status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ function root () {
|
||||||
const paths = [ __dirname, '..', '..' ]
|
const paths = [ __dirname, '..', '..' ]
|
||||||
|
|
||||||
// We are under /dist directory
|
// 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('..')
|
paths.push('..')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,7 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []):
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sanitizeAndCheckVideoCommentObject(body) === false) {
|
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
|
const actorUrl = body.attributedTo
|
||||||
|
|
|
@ -54,6 +54,8 @@ import { ThumbnailModel } from '../../models/video/thumbnail'
|
||||||
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FilteredModelAttributes } from '../../typings/sequelize'
|
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) {
|
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||||
// If the video is not private and is published, we federate it
|
// If the video is not private and is published, we federate it
|
||||||
|
@ -236,72 +238,74 @@ async function updateVideoFromAP (options: {
|
||||||
channel: VideoChannelModel,
|
channel: VideoChannelModel,
|
||||||
overrideTo?: string[]
|
overrideTo?: string[]
|
||||||
}) {
|
}) {
|
||||||
|
const { video, videoObject, account, channel, overrideTo } = options
|
||||||
|
|
||||||
logger.debug('Updating remote video "%s".', options.videoObject.uuid)
|
logger.debug('Updating remote video "%s".', options.videoObject.uuid)
|
||||||
|
|
||||||
let videoFieldsSave: any
|
let videoFieldsSave: any
|
||||||
const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
|
const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE
|
||||||
const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
|
const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let thumbnailModel: ThumbnailModel
|
let thumbnailModel: ThumbnailModel
|
||||||
|
|
||||||
try {
|
try {
|
||||||
thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
|
thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
|
||||||
} catch (err) {
|
} 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 => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
const sequelizeOptions = { transaction: t }
|
const sequelizeOptions = { transaction: t }
|
||||||
|
|
||||||
videoFieldsSave = options.video.toJSON()
|
videoFieldsSave = video.toJSON()
|
||||||
|
|
||||||
// Check actor has the right to update the video
|
// Check actor has the right to update the video
|
||||||
const videoChannel = options.video.VideoChannel
|
const videoChannel = video.VideoChannel
|
||||||
if (videoChannel.Account.id !== options.account.id) {
|
if (videoChannel.Account.id !== account.id) {
|
||||||
throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
|
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 to = overrideTo ? overrideTo : videoObject.to
|
||||||
const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
|
const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
|
||||||
options.video.set('name', videoData.name)
|
video.name = videoData.name
|
||||||
options.video.set('uuid', videoData.uuid)
|
video.uuid = videoData.uuid
|
||||||
options.video.set('url', videoData.url)
|
video.url = videoData.url
|
||||||
options.video.set('category', videoData.category)
|
video.category = videoData.category
|
||||||
options.video.set('licence', videoData.licence)
|
video.licence = videoData.licence
|
||||||
options.video.set('language', videoData.language)
|
video.language = videoData.language
|
||||||
options.video.set('description', videoData.description)
|
video.description = videoData.description
|
||||||
options.video.set('support', videoData.support)
|
video.support = videoData.support
|
||||||
options.video.set('nsfw', videoData.nsfw)
|
video.nsfw = videoData.nsfw
|
||||||
options.video.set('commentsEnabled', videoData.commentsEnabled)
|
video.commentsEnabled = videoData.commentsEnabled
|
||||||
options.video.set('downloadEnabled', videoData.downloadEnabled)
|
video.downloadEnabled = videoData.downloadEnabled
|
||||||
options.video.set('waitTranscoding', videoData.waitTranscoding)
|
video.waitTranscoding = videoData.waitTranscoding
|
||||||
options.video.set('state', videoData.state)
|
video.state = videoData.state
|
||||||
options.video.set('duration', videoData.duration)
|
video.duration = videoData.duration
|
||||||
options.video.set('createdAt', videoData.createdAt)
|
video.createdAt = videoData.createdAt
|
||||||
options.video.set('publishedAt', videoData.publishedAt)
|
video.publishedAt = videoData.publishedAt
|
||||||
options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
|
video.originallyPublishedAt = videoData.originallyPublishedAt
|
||||||
options.video.set('privacy', videoData.privacy)
|
video.privacy = videoData.privacy
|
||||||
options.video.set('channelId', videoData.channelId)
|
video.channelId = videoData.channelId
|
||||||
options.video.set('views', videoData.views)
|
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
|
// FIXME: use icon URL instead
|
||||||
const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
|
const previewUrl = buildRemoteBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.getPreview().filename))
|
||||||
const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
|
const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
|
||||||
await options.video.addAndSaveThumbnail(previewModel, t)
|
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))
|
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
||||||
|
|
||||||
// Remove video files that do not exist anymore
|
// Remove video files that do not exist anymore
|
||||||
const destroyTasks = options.video.VideoFiles
|
const destroyTasks = video.VideoFiles
|
||||||
.filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
|
.filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
|
||||||
.map(f => f.destroy(sequelizeOptions))
|
.map(f => f.destroy(sequelizeOptions))
|
||||||
await Promise.all(destroyTasks)
|
await Promise.all(destroyTasks)
|
||||||
|
|
||||||
// Update or add other one
|
// Update or add other one
|
||||||
|
@ -310,21 +314,17 @@ async function updateVideoFromAP (options: {
|
||||||
.then(([ file ]) => file)
|
.then(([ file ]) => file)
|
||||||
})
|
})
|
||||||
|
|
||||||
options.video.VideoFiles = await Promise.all(upsertTasks)
|
video.VideoFiles = await Promise.all(upsertTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(
|
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(video, videoObject, video.VideoFiles)
|
||||||
options.video,
|
|
||||||
options.videoObject,
|
|
||||||
options.video.VideoFiles
|
|
||||||
)
|
|
||||||
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
|
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
|
||||||
|
|
||||||
// Remove video files that do not exist anymore
|
// Remove video files that do not exist anymore
|
||||||
const destroyTasks = options.video.VideoStreamingPlaylists
|
const destroyTasks = video.VideoStreamingPlaylists
|
||||||
.filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
|
.filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
|
||||||
.map(f => f.destroy(sequelizeOptions))
|
.map(f => f.destroy(sequelizeOptions))
|
||||||
await Promise.all(destroyTasks)
|
await Promise.all(destroyTasks)
|
||||||
|
|
||||||
// Update or add other one
|
// Update or add other one
|
||||||
|
@ -333,36 +333,36 @@ async function updateVideoFromAP (options: {
|
||||||
.then(([ streamingPlaylist ]) => streamingPlaylist)
|
.then(([ streamingPlaylist ]) => streamingPlaylist)
|
||||||
})
|
})
|
||||||
|
|
||||||
options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
|
video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Update Tags
|
// 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)
|
const tagInstances = await TagModel.findOrCreateTags(tags, t)
|
||||||
await options.video.$set('Tags', tagInstances, sequelizeOptions)
|
await video.$set('Tags', tagInstances, sequelizeOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Update captions
|
// Update captions
|
||||||
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)
|
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
|
||||||
|
|
||||||
const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => {
|
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
|
||||||
return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t)
|
return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
|
||||||
})
|
})
|
||||||
options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
|
video.VideoCaptions = await Promise.all(videoCaptionsPromises)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Notify our users?
|
const autoBlacklisted = await autoBlacklistVideoIfNeeded(video, undefined, undefined)
|
||||||
if (wasPrivateVideo || wasUnlistedVideo) {
|
|
||||||
Notifier.Instance.notifyOnNewVideo(options.video)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
} catch (err) {
|
||||||
if (options.video !== undefined && videoFieldsSave !== undefined) {
|
if (video !== undefined && videoFieldsSave !== undefined) {
|
||||||
resetSequelizeInstance(options.video, videoFieldsSave)
|
resetSequelizeInstance(video, videoFieldsSave)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is just a debug because we will retry the insert
|
// 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
|
if (!options.video.isOutdated()) return options.video
|
||||||
|
|
||||||
// We need more attributes if the argument video was fetched with not enough joints
|
// 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 {
|
try {
|
||||||
const { response, videoObject } = await fetchRemoteVideo(video.url)
|
const { response, videoObject } = await fetchRemoteVideo(video.url)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -14,6 +14,10 @@ import { RegisterSettingOptions } from '../../../shared/models/plugins/register-
|
||||||
import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
|
import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
|
||||||
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
|
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
|
||||||
import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-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 {
|
export interface RegisteredPlugin {
|
||||||
npmName: string
|
npmName: string
|
||||||
|
@ -42,7 +46,7 @@ export interface HookInformationValue {
|
||||||
priority: number
|
priority: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PluginManager {
|
export class PluginManager implements ServerHook {
|
||||||
|
|
||||||
private static instance: PluginManager
|
private static instance: PluginManager
|
||||||
|
|
||||||
|
@ -95,25 +99,17 @@ export class PluginManager {
|
||||||
|
|
||||||
// ###################### Hooks ######################
|
// ###################### Hooks ######################
|
||||||
|
|
||||||
async runHook (hookName: string, param?: any) {
|
async runHook (hookName: ServerHookName, param?: any) {
|
||||||
let result = param
|
let result = param
|
||||||
|
|
||||||
if (!this.hooks[hookName]) return result
|
if (!this.hooks[hookName]) return result
|
||||||
|
|
||||||
const wait = hookName.startsWith('static:')
|
const hookType = getHookType(hookName)
|
||||||
|
|
||||||
for (const hook of this.hooks[hookName]) {
|
for (const hook of this.hooks[hookName]) {
|
||||||
try {
|
result = await internalRunHook(hook.handler, hookType, param, err => {
|
||||||
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) {
|
|
||||||
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
|
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as sequelize from 'sequelize'
|
import { Transaction } from 'sequelize'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { UserRight, VideoBlacklistType } from '../../shared/models'
|
import { UserRight, VideoBlacklistType } from '../../shared/models'
|
||||||
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
||||||
|
@ -6,26 +6,39 @@ import { UserModel } from '../models/account/user'
|
||||||
import { VideoModel } from '../models/video/video'
|
import { VideoModel } from '../models/video/video'
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
|
import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
|
||||||
|
import { Hooks } from './plugins/hooks'
|
||||||
|
|
||||||
async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
|
async function autoBlacklistVideoIfNeeded (video: VideoModel, user?: UserModel, transaction?: Transaction) {
|
||||||
if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
|
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 = {
|
const videoBlacklistToCreate = {
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
unfederated: true,
|
unfederated: true,
|
||||||
reason: 'Auto-blacklisted. Moderator review required.',
|
reason: 'Auto-blacklisted. Moderator review required.',
|
||||||
type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
|
type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
|
||||||
}
|
}
|
||||||
await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
|
await VideoBlacklistModel.create(videoBlacklistToCreate, { transaction })
|
||||||
|
|
||||||
logger.info('Video %s auto-blacklisted.', video.uuid)
|
logger.info('Video %s auto-blacklisted.', video.uuid)
|
||||||
|
|
||||||
return true
|
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 {
|
export {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { UserModel } from '../../../models/account/user'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { areValidationErrors } from '../utils'
|
import { areValidationErrors } from '../utils'
|
||||||
|
import { Hooks } from '../../../lib/plugins/hooks'
|
||||||
|
import { isLocalVideoThreadAccepted, isLocalVideoCommentReplyAccepted, AcceptResult } from '../../../lib/moderation'
|
||||||
|
|
||||||
const listVideoCommentThreadsValidator = [
|
const listVideoCommentThreadsValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
||||||
|
@ -48,6 +50,7 @@ const addVideoCommentThreadValidator = [
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||||
if (!isVideoCommentsEnabled(res.locals.video, res)) return
|
if (!isVideoCommentsEnabled(res.locals.video, res)) return
|
||||||
|
if (!await isVideoCommentAccepted(req, res, false)) return
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
@ -65,6 +68,7 @@ const addVideoCommentReplyValidator = [
|
||||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||||
if (!isVideoCommentsEnabled(res.locals.video, res)) return
|
if (!isVideoCommentsEnabled(res.locals.video, res)) return
|
||||||
if (!await doesVideoCommentExist(req.params.commentId, 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 next()
|
||||||
}
|
}
|
||||||
|
@ -193,3 +197,37 @@ function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCom
|
||||||
|
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
||||||
import { authenticatePromiseIfNeeded } from '../../oauth'
|
import { authenticate, authenticatePromiseIfNeeded } from '../../oauth'
|
||||||
import { areValidationErrors } from '../utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
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 { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
|
||||||
import { getServerActor } from '../../../helpers/utils'
|
import { getServerActor } from '../../../helpers/utils'
|
||||||
import { CONFIG } from '../../../initializers/config'
|
import { CONFIG } from '../../../initializers/config'
|
||||||
|
import { isLocalVideoAccepted } from '../../../lib/moderation'
|
||||||
|
import { Hooks } from '../../../lib/plugins/hooks'
|
||||||
|
|
||||||
const videosAddValidator = getCommonVideoEditAttributes().concat([
|
const videosAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
body('videofile')
|
body('videofile')
|
||||||
|
@ -62,14 +64,12 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||||
if (areErrorsInScheduleUpdate(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
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||||
|
|
||||||
const isAble = await user.isAbleToUploadVideo(videoFile)
|
if (await user.isAbleToUploadVideo(videoFile) === false) {
|
||||||
|
|
||||||
if (isAble === false) {
|
|
||||||
res.status(403)
|
res.status(403)
|
||||||
.json({ error: 'The user video quota is exceeded with this video.' })
|
.json({ error: 'The user video quota is exceeded with this video.' })
|
||||||
|
|
||||||
|
@ -88,7 +88,9 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
return cleanUpReqFiles(req)
|
return cleanUpReqFiles(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFile['duration'] = duration
|
videoFile.duration = duration
|
||||||
|
|
||||||
|
if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
@ -434,3 +436,26 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
|
||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -293,7 +293,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
|
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 serverActor = await getServerActor()
|
||||||
const serverAccountId = serverActor.Account.id
|
const serverAccountId = serverActor.Account.id
|
||||||
const userAccountId = user ? user.Account.id : undefined
|
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 serverActor = await getServerActor()
|
||||||
const serverAccountId = serverActor.Account.id
|
const serverAccountId = serverActor.Account.id
|
||||||
const userAccountId = user ? user.Account.id : undefined
|
const userAccountId = user ? user.Account.id : undefined
|
||||||
|
|
|
@ -19,7 +19,17 @@ function compareSemVer (a: string, b: string) {
|
||||||
return segmentsA.length - segmentsB.length
|
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 {
|
export {
|
||||||
randomInt,
|
randomInt,
|
||||||
compareSemVer
|
compareSemVer,
|
||||||
|
isPromise,
|
||||||
|
isCatchable
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum HookType {
|
||||||
|
STATIC = 1,
|
||||||
|
ACTION = 2,
|
||||||
|
FILTER = 3
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -5,7 +5,13 @@
|
||||||
"no-inferrable-types": true,
|
"no-inferrable-types": true,
|
||||||
"eofline": true,
|
"eofline": true,
|
||||||
"indent": [true, "spaces"],
|
"indent": [true, "spaces"],
|
||||||
"ter-indent": [true, 2],
|
"ter-indent": [
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
"max-line-length": [true, 140],
|
"max-line-length": [true, 140],
|
||||||
"no-unused-variable": false, // Memory issues
|
"no-unused-variable": false, // Memory issues
|
||||||
"no-floating-promises": false
|
"no-floating-promises": false
|
||||||
|
|
Loading…
Reference in New Issue