diff --git a/packages/tests/src/api/moderation/video-blacklist.ts b/packages/tests/src/api/moderation/video-blacklist.ts index 2283fbc79..a546bca0c 100644 --- a/packages/tests/src/api/moderation/video-blacklist.ts +++ b/packages/tests/src/api/moderation/video-blacklist.ts @@ -3,12 +3,12 @@ import { expect } from 'chai' import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js' import { sortObjectComparator } from '@peertube/peertube-core-utils' -import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models' +import { HttpStatusCode, UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models' import { BlacklistCommand, cleanupTests, createMultipleServers, - doubleFollow, PeerTubeServer, + doubleFollow, makeActivityPubGetRequest, PeerTubeServer, setAccessTokensToServers, setDefaultChannelAvatar, waitJobs @@ -298,6 +298,13 @@ describe('Test video blacklist', function () { expect(video4Blacklisted.unfederated).to.be.true }) + it('Should not have AP comments/announces/likes/dislikes', async function () { + await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/comments`, HttpStatusCode.UNAUTHORIZED_401) + await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/announces`, HttpStatusCode.UNAUTHORIZED_401) + await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/likes`, HttpStatusCode.UNAUTHORIZED_401) + await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/dislikes`, HttpStatusCode.UNAUTHORIZED_401) + }) + it('Should remove the video from blacklist and refederate the video', async function () { await command.remove({ videoId: video4UUID }) diff --git a/server/core/controllers/activitypub/client.ts b/server/core/controllers/activitypub/client.ts index 09d3e8686..c725f05e3 100644 --- a/server/core/controllers/activitypub/client.ts +++ b/server/core/controllers/activitypub/client.ts @@ -120,7 +120,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity', activityPubClientRouter.get('/videos/watch/:id/announces', executeIfActivityPub, activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(videoAnnouncesController) ) activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', @@ -132,19 +132,19 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', activityPubClientRouter.get('/videos/watch/:id/likes', executeIfActivityPub, activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(videoLikesController) ) activityPubClientRouter.get('/videos/watch/:id/dislikes', executeIfActivityPub, activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(videoDislikesController) ) activityPubClientRouter.get('/videos/watch/:id/comments', executeIfActivityPub, activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(videoCommentsController) ) activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', @@ -175,7 +175,7 @@ activityPubClientRouter.get('/videos/watch/:id/chapters', activityPubRateLimiter, apVideoChaptersSetCacheKey, chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), - asyncMiddleware(videosCustomGetValidator('only-video')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(videoChaptersController) ) @@ -330,7 +330,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo } async function videoAnnouncesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo + const video = res.locals.onlyVideo if (redirectIfNotOwned(video.url, res)) return @@ -347,7 +347,7 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp } async function videoLikesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo + const video = res.locals.onlyVideo if (redirectIfNotOwned(video.url, res)) return @@ -357,7 +357,7 @@ async function videoLikesController (req: express.Request, res: express.Response } async function videoDislikesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo + const video = res.locals.onlyVideo if (redirectIfNotOwned(video.url, res)) return @@ -367,7 +367,7 @@ async function videoDislikesController (req: express.Request, res: express.Respo } async function videoCommentsController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo + const video = res.locals.onlyVideo if (redirectIfNotOwned(video.url, res)) return diff --git a/server/core/controllers/api/videos/chapters.ts b/server/core/controllers/api/videos/chapters.ts index f744a2b56..bbc8ea9c8 100644 --- a/server/core/controllers/api/videos/chapters.ts +++ b/server/core/controllers/api/videos/chapters.ts @@ -11,7 +11,7 @@ import { replaceChapters } from '@server/lib/video-chapters.js' const videoChaptersRouter = express.Router() videoChaptersRouter.get('/:id/chapters', - asyncMiddleware(videosCustomGetValidator('only-video')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), asyncMiddleware(listVideoChapters) ) diff --git a/server/core/controllers/api/videos/ownership.ts b/server/core/controllers/api/videos/ownership.ts index ceb3a2739..77aefe516 100644 --- a/server/core/controllers/api/videos/ownership.ts +++ b/server/core/controllers/api/videos/ownership.ts @@ -1,6 +1,7 @@ -import express from 'express' -import { HttpStatusCode, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models' +import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models' +import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js' import { MVideoFullLight } from '@server/types/models/index.js' +import express from 'express' import { logger } from '../../../helpers/logger.js' import { getFormattedObjects } from '../../../helpers/utils.js' import { sequelizeTypescript } from '../../../initializers/database.js' @@ -113,7 +114,7 @@ function acceptOwnership (req: express.Request, res: express.Response) { const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight targetVideoUpdated.VideoChannel = channel - if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) { + if (canVideoBeFederated(targetVideoUpdated)) { await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) } diff --git a/server/core/controllers/api/videos/token.ts b/server/core/controllers/api/videos/token.ts index 9892518aa..7a4260954 100644 --- a/server/core/controllers/api/videos/token.ts +++ b/server/core/controllers/api/videos/token.ts @@ -7,7 +7,7 @@ const tokenRouter = express.Router() tokenRouter.post('/:id/token', optionalAuthenticate, - asyncMiddleware(videosCustomGetValidator('only-video')), + asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')), videoFileTokenValidator, generateToken ) diff --git a/server/core/controllers/api/videos/update.ts b/server/core/controllers/api/videos/update.ts index 8a191e59e..2bc34d904 100644 --- a/server/core/controllers/api/videos/update.ts +++ b/server/core/controllers/api/videos/update.ts @@ -1,9 +1,11 @@ -import express, { UploadFiles } from 'express' -import { Transaction } from 'sequelize' import { forceNumber } from '@peertube/peertube-core-utils' import { HttpStatusCode, ThumbnailType, VideoPrivacy, VideoPrivacyType, VideoUpdate } from '@peertube/peertube-models' import { exists } from '@server/helpers/custom-validators/misc.js' import { changeVideoChannelShare } from '@server/lib/activitypub/share.js' +import { isNewVideoPrivacyForFederation, isPrivacyForFederation } from '@server/lib/activitypub/videos/federate.js' +import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js' +import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js' +import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js' import { VideoPathManager } from '@server/lib/video-path-manager.js' import { setVideoPrivacy } from '@server/lib/video-privacy.js' import { setVideoTags } from '@server/lib/video.js' @@ -11,7 +13,9 @@ import { openapiOperationDoc } from '@server/middlewares/doc.js' import { VideoPasswordModel } from '@server/models/video/video-password.js' import { FilteredModelAttributes } from '@server/types/index.js' import { MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js' -import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js' +import express, { UploadFiles } from 'express' +import { Transaction } from 'sequelize' +import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js' import { resetSequelizeInstance } from '../../../helpers/database-utils.js' import { createReqFiles } from '../../../helpers/express-utils.js' import { logger, loggerTagsFactory } from '../../../helpers/logger.js' @@ -22,9 +26,6 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist.js' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares/index.js' import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js' import { VideoModel } from '../../../models/video/video.js' -import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js' -import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js' -import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js' const lTags = loggerTagsFactory('api', 'video') const auditLogger = auditLoggerFactory('videos') @@ -53,7 +54,7 @@ async function updateVideo (req: express.Request, res: express.Response) { const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) const videoInfoToUpdate: VideoUpdate = req.body - const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() + const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy) const oldPrivacy = videoFromReq.privacy const thumbnails = await buildVideoThumbnailsFromReq(videoFromReq, req.files) @@ -191,7 +192,7 @@ async function updateVideoPrivacy (options: { transaction: Transaction }) { const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options - const isNewVideoForFederation = videoInstance.isNewVideoForFederation(videoInfoToUpdate.privacy) + const isNewVideoForFederation = isNewVideoPrivacyForFederation(videoInfoToUpdate.privacy, videoInfoToUpdate.privacy) const newPrivacy = forceNumber(videoInfoToUpdate.privacy) as VideoPrivacyType setVideoPrivacy(videoInstance, newPrivacy) @@ -207,7 +208,7 @@ async function updateVideoPrivacy (options: { } // Unfederate the video if the new privacy is not compatible with federation - if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { + if (hadPrivacyForFederation && !isPrivacyForFederation(videoInstance.privacy)) { await VideoModel.sendDelete(videoInstance, { transaction }) } diff --git a/server/core/helpers/video.ts b/server/core/helpers/video.ts index dc56a6697..36e77f140 100644 --- a/server/core/helpers/video.ts +++ b/server/core/helpers/video.ts @@ -1,51 +1,28 @@ -import { Response } from 'express' -import { forceNumber } from '@peertube/peertube-core-utils' -import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models' +import { VideoPrivacy } from '@peertube/peertube-models' import { CONFIG } from '@server/initializers/config.js' import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.js' +import { Response } from 'express' -function getVideoWithAttributes (res: Response) { +export function getVideoWithAttributes (res: Response) { return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo } -function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { +export function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { return isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.Video : videoOrPlaylist } -function isPrivacyForFederation (privacy: VideoPrivacyType) { - const castedPrivacy = forceNumber(privacy) - - return castedPrivacy === VideoPrivacy.PUBLIC || - (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) -} - -function isStateForFederation (state: VideoStateType) { - const castedState = forceNumber(state) - - return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED -} - -function getPrivaciesForFederation () { +export function getPrivaciesForFederation () { return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true) ? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ] : [ { privacy: VideoPrivacy.PUBLIC } ] } -function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) { +export function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) { const value = mimeTypes[mimeType] if (Array.isArray(value)) return value[0] return value } - -export { - getVideoWithAttributes, - extractVideo, - getExtFromMimetype, - isStateForFederation, - isPrivacyForFederation, - getPrivaciesForFederation -} diff --git a/server/core/lib/activitypub/playlists/create-update.ts b/server/core/lib/activitypub/playlists/create-update.ts index 05fef5df1..7de5664bd 100644 --- a/server/core/lib/activitypub/playlists/create-update.ts +++ b/server/core/lib/activitypub/playlists/create-update.ts @@ -145,7 +145,7 @@ async function buildElementsDBAttributes (elementUrls: string[], playlist: MVide try { const { elementObject } = await fetchRemotePlaylistElement(elementUrl) - const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' }) + const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video-and-blacklist' }) elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video)) } catch (err) { diff --git a/server/core/lib/activitypub/process/process-create.ts b/server/core/lib/activitypub/process/process-create.ts index 6e068c95b..5f031b7fa 100644 --- a/server/core/lib/activitypub/process/process-create.ts +++ b/server/core/lib/activitypub/process/process-create.ts @@ -24,7 +24,7 @@ import { createOrUpdateLocalVideoViewer } from '../local-video-viewer.js' import { createOrUpdateVideoPlaylist } from '../playlists/index.js' import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js' import { resolveThread } from '../video-comments.js' -import { getOrCreateAPVideo } from '../videos/index.js' +import { canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js' async function processCreateActivity (options: APProcessorOptions>) { const { activity, byActor } = options @@ -87,6 +87,11 @@ async function processCreateCacheFile ( const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) + if (video.isOwned() && !canVideoBeFederated(video)) { + logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`) + return + } + await sequelizeTypescript.transaction(async t => { return createOrUpdateCacheFile(cacheFile, video, byActor, t) }) diff --git a/server/core/lib/activitypub/process/process-dislike.ts b/server/core/lib/activitypub/process/process-dislike.ts index 9182e6de4..fc62e9bd6 100644 --- a/server/core/lib/activitypub/process/process-dislike.ts +++ b/server/core/lib/activitypub/process/process-dislike.ts @@ -1,11 +1,12 @@ -import { VideoModel } from '@server/models/video/video.js' import { ActivityDislike } from '@peertube/peertube-models' +import { logger } from '@server/helpers/logger.js' +import { VideoModel } from '@server/models/video/video.js' import { retryTransactionWrapper } from '../../../helpers/database-utils.js' import { sequelizeTypescript } from '../../../initializers/database.js' import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js' import { APProcessorOptions } from '../../../types/activitypub-processor.model.js' import { MActorSignature } from '../../../types/models/index.js' -import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js' +import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js' async function processDislikeActivity (options: APProcessorOptions) { const { activity, byActor } = options @@ -21,14 +22,19 @@ export { // --------------------------------------------------------------------------- async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { - const dislikeObject = activity.object + const videoUrl = activity.object const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) - const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' }) + const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' }) if (!onlyVideo?.isOwned()) return + if (!canVideoBeFederated(onlyVideo)) { + logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`) + return + } + return sequelizeTypescript.transaction(async t => { const video = await VideoModel.loadFull(onlyVideo.id, t) diff --git a/server/core/lib/activitypub/process/process-like.ts b/server/core/lib/activitypub/process/process-like.ts index ccdb37a02..06e883533 100644 --- a/server/core/lib/activitypub/process/process-like.ts +++ b/server/core/lib/activitypub/process/process-like.ts @@ -1,4 +1,5 @@ import { ActivityLike } from '@peertube/peertube-models' +import { logger } from '@server/helpers/logger.js' import { VideoModel } from '@server/models/video/video.js' import { retryTransactionWrapper } from '../../../helpers/database-utils.js' import { sequelizeTypescript } from '../../../initializers/database.js' @@ -6,7 +7,7 @@ import { getAPId } from '../../../lib/activitypub/activity.js' import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js' import { APProcessorOptions } from '../../../types/activitypub-processor.model.js' import { MActorSignature } from '../../../types/models/index.js' -import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js' +import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js' async function processLikeActivity (options: APProcessorOptions) { const { activity, byActor } = options @@ -28,9 +29,14 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) - const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' }) + const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' }) if (!onlyVideo?.isOwned()) return + if (!canVideoBeFederated(onlyVideo)) { + logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`) + return + } + return sequelizeTypescript.transaction(async t => { const video = await VideoModel.loadFull(onlyVideo.id, t) diff --git a/server/core/lib/activitypub/process/process-update.ts b/server/core/lib/activitypub/process/process-update.ts index d92b347ab..e8a5dcb60 100644 --- a/server/core/lib/activitypub/process/process-update.ts +++ b/server/core/lib/activitypub/process/process-update.ts @@ -20,7 +20,7 @@ import { APActorUpdater } from '../actors/updater.js' import { createOrUpdateCacheFile } from '../cache-file.js' import { createOrUpdateVideoPlaylist } from '../playlists/index.js' import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js' -import { APVideoUpdater, getOrCreateAPVideo } from '../videos/index.js' +import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js' async function processUpdateActivity (options: APProcessorOptions>) { const { activity, byActor } = options @@ -93,6 +93,11 @@ async function processUpdateCacheFile ( const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) + if (video.isOwned() && !canVideoBeFederated(video)) { + logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`) + return + } + await sequelizeTypescript.transaction(async t => { await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) }) diff --git a/server/core/lib/activitypub/process/process-view.ts b/server/core/lib/activitypub/process/process-view.ts index 60820e249..2687eec30 100644 --- a/server/core/lib/activitypub/process/process-view.ts +++ b/server/core/lib/activitypub/process/process-view.ts @@ -24,7 +24,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu const { video } = await getOrCreateAPVideo({ videoObject, - fetchType: 'only-video', + fetchType: 'only-video-and-blacklist', allowRefresh: false }) diff --git a/server/core/lib/activitypub/send/send-create.ts b/server/core/lib/activitypub/send/send-create.ts index 7ab567d03..edffe7fca 100644 --- a/server/core/lib/activitypub/send/send-create.ts +++ b/server/core/lib/activitypub/send/send-create.ts @@ -1,5 +1,3 @@ -import { Transaction } from 'sequelize' -import { getServerActor } from '@server/models/application/application.js' import { ActivityAudience, ActivityCreate, @@ -9,19 +7,24 @@ import { VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' +import { AccountModel } from '@server/models/account/account.js' +import { getServerActor } from '@server/models/application/application.js' +import { VideoModel } from '@server/models/video/video.js' +import { Transaction } from 'sequelize' import { logger, loggerTagsFactory } from '../../../helpers/logger.js' import { VideoCommentModel } from '../../../models/video/video-comment.js' import { MActorLight, MCommentOwnerVideo, MLocalVideoViewerWithWatchSections, - MVideoAccountLight, MVideoAP, + MVideoAccountLight, MVideoPlaylistFull, MVideoRedundancyFileVideo, MVideoRedundancyStreamingPlaylistVideo } from '../../../types/models/index.js' import { audiencify, getAudience } from '../audience.js' +import { canVideoBeFederated } from '../videos/federate.js' import { broadcastToActors, broadcastToFollowers, @@ -32,12 +35,11 @@ import { sendVideoRelatedActivity, unicastTo } from './shared/index.js' -import { AccountModel } from '@server/models/account/account.js' const lTags = loggerTagsFactory('ap', 'create') -async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { - if (!video.hasPrivacyForFederation()) return undefined +export async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { + if (!canVideoBeFederated(video)) return undefined logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) @@ -56,7 +58,7 @@ async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { }) } -async function sendCreateCacheFile ( +export async function sendCreateCacheFile ( byActor: MActorLight, video: MVideoAccountLight, fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo @@ -72,7 +74,7 @@ async function sendCreateCacheFile ( }) } -async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) { +export async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) { logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid)) const byActor = await getServerActor() @@ -84,7 +86,7 @@ async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' }) } -async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) { +export async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) { if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid)) @@ -109,11 +111,20 @@ async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transactio }) } -async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) { - logger.info('Creating job to send comment %s.', comment.url) - +export async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) { const isOrigin = comment.Video.isOwned() + if (isOrigin) { + const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id) + + if (!canVideoBeFederated(videoWithBlacklist)) { + logger.debug(`Do not send comment ${comment.url} on a video that cannot be federated`) + return undefined + } + } + + logger.info('Creating job to send comment %s.', comment.url) + const byActor = comment.Account.Actor const videoAccount = await AccountModel.load(comment.Video.VideoChannel.Account.id, transaction) @@ -179,7 +190,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: }) } -function buildCreateActivity ( +export function buildCreateActivity ( url: string, byActor: MActorLight, object: T, @@ -201,16 +212,7 @@ function buildCreateActivity ( } // --------------------------------------------------------------------------- - -export { - sendCreateVideo, - buildCreateActivity, - sendCreateVideoComment, - sendCreateVideoPlaylist, - sendCreateCacheFile, - sendCreateWatchAction -} - +// Private // --------------------------------------------------------------------------- async function sendVideoRelatedCreateActivity (options: { diff --git a/server/core/lib/activitypub/send/send-update.ts b/server/core/lib/activitypub/send/send-update.ts index f6b714c16..0320afb4c 100644 --- a/server/core/lib/activitypub/send/send-update.ts +++ b/server/core/lib/activitypub/send/send-update.ts @@ -1,10 +1,10 @@ -import { Transaction } from 'sequelize' -import { getServerActor } from '@server/models/application/application.js' import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' +import { getServerActor } from '@server/models/application/application.js' +import { Transaction } from 'sequelize' import { logger } from '../../../helpers/logger.js' import { AccountModel } from '../../../models/account/account.js' -import { VideoModel } from '../../../models/video/video.js' import { VideoShareModel } from '../../../models/video/video-share.js' +import { VideoModel } from '../../../models/video/video.js' import { MAccountDefault, MActor, @@ -16,11 +16,12 @@ import { } from '../../../types/models/index.js' import { audiencify, getAudience } from '../audience.js' import { getUpdateActivityPubUrl } from '../url.js' +import { canVideoBeFederated } from '../videos/federate.js' import { getActorsInvolvedInVideo } from './shared/index.js' import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js' -async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) { - if (!videoArg.hasPrivacyForFederation()) return undefined +export async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) { + if (!canVideoBeFederated(videoArg)) return undefined const video = await videoArg.lightAPToFullAP(transaction) @@ -47,7 +48,7 @@ async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transactio }) } -async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) { +export async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) { const byActor = accountOrChannel.Actor logger.info('Creating job to update actor %s.', byActor.url) @@ -77,7 +78,7 @@ async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefa }) } -async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) { +export async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) { logger.info('Creating job to update cache file %s.', redundancyModel.url) const associatedVideo = redundancyModel.getVideo() @@ -98,7 +99,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' }) } -async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) { +export async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) { if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined const byActor = videoPlaylist.OwnerAccount.Actor @@ -127,14 +128,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, trans } // --------------------------------------------------------------------------- - -export { - sendUpdateActor, - sendUpdateVideo, - sendUpdateCacheFile, - sendUpdateVideoPlaylist -} - +// Private // --------------------------------------------------------------------------- function buildUpdateActivity ( diff --git a/server/core/lib/activitypub/share.ts b/server/core/lib/activitypub/share.ts index b1e49861e..b62b67574 100644 --- a/server/core/lib/activitypub/share.ts +++ b/server/core/lib/activitypub/share.ts @@ -1,6 +1,6 @@ +import { getServerActor } from '@server/models/application/application.js' import Bluebird from 'bluebird' import { Transaction } from 'sequelize' -import { getServerActor } from '@server/models/application/application.js' import { logger, loggerTagsFactory } from '../../helpers/logger.js' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants.js' import { VideoShareModel } from '../../models/video/video-share.js' @@ -12,16 +12,7 @@ import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url.js const lTags = loggerTagsFactory('share') -async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { - if (!video.hasPrivacyForFederation()) return undefined - - return Promise.all([ - shareByServer(video, t), - shareByVideoChannel(video, t) - ]) -} - -async function changeVideoChannelShare ( +export async function changeVideoChannelShare ( video: MVideoAccountLight, oldVideoChannel: MChannelActorLight, t: Transaction @@ -36,7 +27,7 @@ async function changeVideoChannelShare ( await shareByVideoChannel(video, t) } -async function addVideoShares (shareUrls: string[], video: MVideoId) { +export async function addVideoShares (shareUrls: string[], video: MVideoId) { await Bluebird.map(shareUrls, async shareUrl => { try { await addVideoShare(shareUrl, video) @@ -46,12 +37,44 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) { }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) } -export { - changeVideoChannelShare, - addVideoShares, - shareVideoByServerAndChannel +export async function shareByServer (video: MVideo, t: Transaction) { + const serverActor = await getServerActor() + + const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video) + const [ serverShare ] = await VideoShareModel.findOrCreate({ + defaults: { + actorId: serverActor.id, + videoId: video.id, + url: serverShareUrl + }, + where: { + url: serverShareUrl + }, + transaction: t + }) + + return sendVideoAnnounce(serverActor, serverShare, video, t) } +export async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) { + const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) + const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ + defaults: { + actorId: video.VideoChannel.actorId, + videoId: video.id, + url: videoChannelShareUrl + }, + where: { + url: videoChannelShareUrl + }, + transaction: t + }) + + return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) +} + +// --------------------------------------------------------------------------- +// Private // --------------------------------------------------------------------------- async function addVideoShare (shareUrl: string, video: MVideoId) { @@ -74,42 +97,6 @@ async function addVideoShare (shareUrl: string, video: MVideoId) { await VideoShareModel.upsert(entry) } -async function shareByServer (video: MVideo, t: Transaction) { - const serverActor = await getServerActor() - - const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video) - const [ serverShare ] = await VideoShareModel.findOrCreate({ - defaults: { - actorId: serverActor.id, - videoId: video.id, - url: serverShareUrl - }, - where: { - url: serverShareUrl - }, - transaction: t - }) - - return sendVideoAnnounce(serverActor, serverShare, video, t) -} - -async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) { - const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) - const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ - defaults: { - actorId: video.VideoChannel.actorId, - videoId: video.id, - url: videoChannelShareUrl - }, - where: { - url: videoChannelShareUrl - }, - transaction: t - }) - - return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) -} - async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) { // Load old share const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t) diff --git a/server/core/lib/activitypub/video-comments.ts b/server/core/lib/activitypub/video-comments.ts index f3072988a..f710f9b97 100644 --- a/server/core/lib/activitypub/video-comments.ts +++ b/server/core/lib/activitypub/video-comments.ts @@ -9,7 +9,7 @@ import { Hooks } from '../plugins/hooks.js' import { fetchAP } from './activity.js' import { getOrCreateAPActor } from './actors/index.js' import { checkUrlsSameHost } from './url.js' -import { getOrCreateAPVideo } from './videos/index.js' +import { canVideoBeFederated, getOrCreateAPVideo } from './videos/index.js' type ResolveThreadParams = { url: string @@ -92,8 +92,8 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false } const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) - if (video.isOwned() && !video.hasPrivacyForFederation()) { - throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') + if (video.isOwned() && !canVideoBeFederated(video)) { + throw new Error('Cannot resolve thread of video that is not compatible with federation') } let resultComment: MCommentOwnerVideo diff --git a/server/core/lib/activitypub/videos/federate.ts b/server/core/lib/activitypub/videos/federate.ts index 76b9030f1..c366a1dbb 100644 --- a/server/core/lib/activitypub/videos/federate.ts +++ b/server/core/lib/activitypub/videos/federate.ts @@ -1,29 +1,53 @@ +import { forceNumber } from '@peertube/peertube-core-utils' +import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models' +import { CONFIG } from '@server/initializers/config.js' +import { MVideoAPLight, MVideoWithBlacklistRights } from '@server/types/models/index.js' import { Transaction } from 'sequelize' -import { MVideoAP, MVideoAPLight } from '@server/types/models/index.js' import { sendCreateVideo, sendUpdateVideo } from '../send/index.js' -import { shareVideoByServerAndChannel } from '../share.js' +import { shareByServer, shareByVideoChannel } from '../share.js' -async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) { - const video = videoArg as MVideoAP +export async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) { + if (!canVideoBeFederated(videoArg, isNewVideo)) return - if ( - // Check this is not a blacklisted video, or unfederated blacklisted video - (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && - // Check the video is public/unlisted and published - video.hasPrivacyForFederation() && video.hasStateForFederation() - ) { - const video = await videoArg.lightAPToFullAP(transaction) + const video = await videoArg.lightAPToFullAP(transaction) - if (isNewVideo) { - // Now we'll add the video's meta data to our followers - await sendCreateVideo(video, transaction) - await shareVideoByServerAndChannel(video, transaction) - } else { - await sendUpdateVideo(video, transaction) - } + if (isNewVideo) { + // Now we'll add the video's meta data to our followers + await sendCreateVideo(video, transaction) + + await Promise.all([ + shareByServer(video, transaction), + shareByVideoChannel(video, transaction) + ]) + } else { + await sendUpdateVideo(video, transaction) } } -export { - federateVideoIfNeeded +export function canVideoBeFederated (video: MVideoWithBlacklistRights, isNewVideo = false) { + // Check this is not a blacklisted video + if (video.isBlacklisted() === true) { + if (isNewVideo === false) return false + if (video.VideoBlacklist.unfederated === true) return false + } + + // Check the video is public/unlisted and published + return isPrivacyForFederation(video.privacy) && isStateForFederation(video.state) +} + +export function isNewVideoPrivacyForFederation (currentPrivacy: VideoPrivacyType, newPrivacy: VideoPrivacyType) { + return !isPrivacyForFederation(currentPrivacy) && isPrivacyForFederation(newPrivacy) +} + +export function isPrivacyForFederation (privacy: VideoPrivacyType) { + const castedPrivacy = forceNumber(privacy) + + return castedPrivacy === VideoPrivacy.PUBLIC || + (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) +} + +export function isStateForFederation (state: VideoStateType) { + const castedState = forceNumber(state) + + return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED } diff --git a/server/core/lib/activitypub/videos/get.ts b/server/core/lib/activitypub/videos/get.ts index 8cc5b69d3..d667b433f 100644 --- a/server/core/lib/activitypub/videos/get.ts +++ b/server/core/lib/activitypub/videos/get.ts @@ -1,9 +1,14 @@ +import { APObjectId } from '@peertube/peertube-models' import { retryTransactionWrapper } from '@server/helpers/database-utils.js' import { logger } from '@server/helpers/logger.js' import { JobQueue } from '@server/lib/job-queue/index.js' import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js' -import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models/index.js' -import { APObjectId } from '@peertube/peertube-models' +import { + MVideoAccountLightBlacklistAllFiles, + MVideoImmutable, + MVideoThumbnail, + MVideoThumbnailBlacklist +} from '@server/types/models/index.js' import { getAPId } from '../activity.js' import { refreshVideoIfNeeded } from './refresh.js' import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js' @@ -24,23 +29,25 @@ type GetVideoParamAll = { type GetVideoParamImmutable = { videoObject: APObjectId syncParam?: SyncParam - fetchType: 'only-immutable-attributes' + fetchType: 'unsafe-only-immutable-attributes' allowRefresh: false } type GetVideoParamOther = { videoObject: APObjectId syncParam?: SyncParam - fetchType?: 'all' | 'only-video' + fetchType?: 'all' | 'only-video-and-blacklist' allowRefresh?: boolean } export function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult export function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult -export function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult +export function getOrCreateAPVideo ( + options: GetVideoParamOther +): GetVideoResult export async function getOrCreateAPVideo ( options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther -): GetVideoResult { +): GetVideoResult { // Default params const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false } const fetchType = options.fetchType || 'all' @@ -52,7 +59,7 @@ export async function getOrCreateAPVideo ( if (videoFromDatabase) { if (allowRefresh === true) { - // Typings ensure allowRefresh === false in only-immutable-attributes fetch type + // Typings ensure allowRefresh === false in unsafe-only-immutable-attributes fetch type videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam) } @@ -87,7 +94,9 @@ export async function getOrCreateAPVideo ( export function maybeGetOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult export function maybeGetOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult -export function maybeGetOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult +export function maybeGetOrCreateAPVideo ( + options: GetVideoParamOther +): GetVideoResult export async function maybeGetOrCreateAPVideo (options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther) { try { const result = await getOrCreateAPVideo(options as any) diff --git a/server/core/lib/model-loaders/video.ts b/server/core/lib/model-loaders/video.ts index ad8e87155..6e4626c8b 100644 --- a/server/core/lib/model-loaders/video.ts +++ b/server/core/lib/model-loaders/video.ts @@ -6,57 +6,57 @@ import { MVideoFullLight, MVideoId, MVideoImmutable, - MVideoThumbnail + MVideoThumbnailBlacklist } from '@server/types/models/index.js' import { getOrCreateAPVideo } from '../activitypub/videos/get.js' -type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes' +type VideoLoadType = 'for-api' | 'all' | 'only-video-and-blacklist' | 'id' | 'none' | 'unsafe-only-immutable-attributes' function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise -function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise -function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise +function loadVideo (id: number | string, fetchType: 'unsafe-only-immutable-attributes'): Promise +function loadVideo (id: number | string, fetchType: 'only-video-and-blacklist', userId?: number): Promise function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise function loadVideo ( id: number | string, fetchType: VideoLoadType, userId?: number -): Promise +): Promise function loadVideo ( id: number | string, fetchType: VideoLoadType, userId?: number -): Promise { +): Promise { if (fetchType === 'for-api') return VideoModel.loadForGetAPI({ id, userId }) if (fetchType === 'all') return VideoModel.loadFull(id, undefined, userId) - if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id) + if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadImmutableAttributes(id) - if (fetchType === 'only-video') return VideoModel.load(id) + if (fetchType === 'only-video-and-blacklist') return VideoModel.loadWithBlacklist(id) if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) } -type VideoLoadByUrlType = 'all' | 'only-video' | 'only-immutable-attributes' +type VideoLoadByUrlType = 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes' function loadVideoByUrl (url: string, fetchType: 'all'): Promise -function loadVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Promise -function loadVideoByUrl (url: string, fetchType: 'only-video'): Promise +function loadVideoByUrl (url: string, fetchType: 'unsafe-only-immutable-attributes'): Promise +function loadVideoByUrl (url: string, fetchType: 'only-video-and-blacklist'): Promise function loadVideoByUrl ( url: string, fetchType: VideoLoadByUrlType -): Promise +): Promise function loadVideoByUrl ( url: string, fetchType: VideoLoadByUrlType -): Promise { +): Promise { if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccountAndFiles(url) - if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url) + if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url) - if (fetchType === 'only-video') return VideoModel.loadByUrl(url) + if (fetchType === 'only-video-and-blacklist') return VideoModel.loadByUrlWithBlacklist(url) } async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) { @@ -64,7 +64,7 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) { try { const res = await getOrCreateAPVideo({ videoObject: videoUrl, - fetchType: 'only-immutable-attributes', + fetchType: 'unsafe-only-immutable-attributes', allowRefresh: false }) @@ -78,10 +78,8 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) { } export { - type VideoLoadType, - type VideoLoadByUrlType, - - loadVideo, + loadOrCreateVideoIfAllowedForUser, loadVideo, loadVideoByUrl, - loadOrCreateVideoIfAllowedForUser + type VideoLoadByUrlType, + type VideoLoadType } diff --git a/server/core/lib/schedulers/update-videos-scheduler.ts b/server/core/lib/schedulers/update-videos-scheduler.ts index d2cebdbd4..0e2483842 100644 --- a/server/core/lib/schedulers/update-videos-scheduler.ts +++ b/server/core/lib/schedulers/update-videos-scheduler.ts @@ -5,10 +5,11 @@ import { logger } from '../../helpers/logger.js' import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants.js' import { sequelizeTypescript } from '../../initializers/database.js' import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update.js' +import { isNewVideoPrivacyForFederation } from '../activitypub/videos/federate.js' import { Notifier } from '../notifier/index.js' +import { addVideoJobsAfterUpdate } from '../video-jobs.js' import { VideoPathManager } from '../video-path-manager.js' import { setVideoPrivacy } from '../video-privacy.js' -import { addVideoJobsAfterUpdate } from '../video-jobs.js' import { AbstractScheduler } from './abstract-scheduler.js' export class UpdateVideosScheduler extends AbstractScheduler { @@ -58,7 +59,7 @@ export class UpdateVideosScheduler extends AbstractScheduler { logger.info('Executing scheduled video update on %s.', video.uuid) if (schedule.privacy) { - isNewVideoForFederation = video.isNewVideoForFederation(schedule.privacy) + isNewVideoForFederation = isNewVideoPrivacyForFederation(video.privacy, schedule.privacy) oldPrivacy = video.privacy setVideoPrivacy(video, schedule.privacy) diff --git a/server/core/middlewares/validators/metrics.ts b/server/core/middlewares/validators/metrics.ts index 6e1e11952..8832b80d2 100644 --- a/server/core/middlewares/validators/metrics.ts +++ b/server/core/middlewares/validators/metrics.ts @@ -47,7 +47,7 @@ const addPlaybackMetricValidator = [ const body: PlaybackMetricCreate = req.body if (areValidationErrors(req, res)) return - if (!await doesVideoExist(body.videoId, res, 'only-immutable-attributes')) return + if (!await doesVideoExist(body.videoId, res, 'unsafe-only-immutable-attributes')) return return next() } diff --git a/server/core/middlewares/validators/redundancy.ts b/server/core/middlewares/validators/redundancy.ts index d46a645f5..27c0591da 100644 --- a/server/core/middlewares/validators/redundancy.ts +++ b/server/core/middlewares/validators/redundancy.ts @@ -143,7 +143,7 @@ const addVideoRedundancyValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return + if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return if (res.locals.onlyVideo.remote === false) { return res.fail({ message: 'Cannot create a redundancy on a local video' }) diff --git a/server/core/middlewares/validators/shared/videos.ts b/server/core/middlewares/validators/shared/videos.ts index 1e6a2e7ba..261fd15b0 100644 --- a/server/core/middlewares/validators/shared/videos.ts +++ b/server/core/middlewares/validators/shared/videos.ts @@ -47,7 +47,7 @@ export async function doesVideoExist (id: number | string, res: Response, fetchT res.locals.videoAll = video as MVideoFullLight break - case 'only-immutable-attributes': + case 'unsafe-only-immutable-attributes': res.locals.onlyImmutableVideo = video as MVideoImmutable break @@ -55,7 +55,7 @@ export async function doesVideoExist (id: number | string, res: Response, fetchT res.locals.videoId = video as MVideoId break - case 'only-video': + case 'only-video-and-blacklist': res.locals.onlyVideo = video as MVideoThumbnail break } diff --git a/server/core/middlewares/validators/videos/video-captions.ts b/server/core/middlewares/validators/videos/video-captions.ts index d89b48f5f..d6407502b 100644 --- a/server/core/middlewares/validators/videos/video-captions.ts +++ b/server/core/middlewares/validators/videos/video-captions.ts @@ -67,7 +67,7 @@ const listVideoCaptionsValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return + if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return const video = res.locals.onlyVideo if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return diff --git a/server/core/middlewares/validators/videos/video-comments.ts b/server/core/middlewares/validators/videos/video-comments.ts index 284d87e85..37da4d488 100644 --- a/server/core/middlewares/validators/videos/video-comments.ts +++ b/server/core/middlewares/validators/videos/video-comments.ts @@ -56,7 +56,7 @@ const listVideoCommentThreadsValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return + if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return @@ -73,7 +73,7 @@ const listVideoThreadCommentsValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return + if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return diff --git a/server/core/middlewares/validators/videos/video-playlists.ts b/server/core/middlewares/validators/videos/video-playlists.ts index 1e4998101..45d87910a 100644 --- a/server/core/middlewares/validators/videos/video-playlists.ts +++ b/server/core/middlewares/validators/videos/video-playlists.ts @@ -205,7 +205,7 @@ const videoPlaylistsAddVideoValidator = [ if (areValidationErrors(req, res)) return if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return - if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return + if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return const videoPlaylist = getPlaylist(res) diff --git a/server/core/middlewares/validators/videos/video-view.ts b/server/core/middlewares/validators/videos/video-view.ts index bcd48b518..4f2c4779b 100644 --- a/server/core/middlewares/validators/videos/video-view.ts +++ b/server/core/middlewares/validators/videos/video-view.ts @@ -37,7 +37,7 @@ export const videoViewValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.params.videoId, res, 'only-immutable-attributes')) return + if (!await doesVideoExist(req.params.videoId, res, 'unsafe-only-immutable-attributes')) return const video = res.locals.onlyImmutableVideo const { duration } = await getCachedVideoDuration(video.id) diff --git a/server/core/middlewares/validators/videos/videos.ts b/server/core/middlewares/validators/videos/videos.ts index a52800e38..a3b4793fb 100644 --- a/server/core/middlewares/validators/videos/videos.ts +++ b/server/core/middlewares/validators/videos/videos.ts @@ -243,7 +243,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R }) } -const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes') => { +const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes') => { return [ isValidVideoIdParam('id'), @@ -254,7 +254,7 @@ const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | if (!await doesVideoExist(req.params.id, res, fetchType)) return // Controllers does not need to check video rights - if (fetchType === 'only-immutable-attributes') return next() + if (fetchType === 'unsafe-only-immutable-attributes') return next() const video = getVideoWithAttributes(res) as MVideoFullLight diff --git a/server/core/models/video/video.ts b/server/core/models/video/video.ts index 22643bbbb..3cdf1666c 100644 --- a/server/core/models/video/video.ts +++ b/server/core/models/video/video.ts @@ -19,7 +19,7 @@ import { type VideoStateType } from '@peertube/peertube-models' import { uuidToShort } from '@peertube/peertube-node-utils' -import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video.js' +import { getPrivaciesForFederation } from '@server/helpers/video.js' import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js' import { LiveManager } from '@server/lib/live/live-manager.js' import { @@ -1448,6 +1448,12 @@ export class VideoModel extends SequelizeModel { return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' }) } + static loadByUrlWithBlacklist (url: string, transaction?: Transaction): Promise { + const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize) + + return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails-blacklist' }) + } + static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise { const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize) @@ -2045,18 +2051,6 @@ export class VideoModel extends SequelizeModel { return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL) } - hasPrivacyForFederation () { - return isPrivacyForFederation(this.privacy) - } - - hasStateForFederation () { - return isStateForFederation(this.state) - } - - isNewVideoForFederation (newPrivacy: VideoPrivacyType) { - return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true - } - setAsRefreshed (transaction?: Transaction) { return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) } diff --git a/server/core/types/models/video/video.ts b/server/core/types/models/video/video.ts index d6f62648a..010009af2 100644 --- a/server/core/types/models/video/video.ts +++ b/server/core/types/models/video/video.ts @@ -102,6 +102,10 @@ export type MVideoWithBlacklistLight = MVideo & Use<'VideoBlacklist', MVideoBlacklistLight> +export type MVideoWithBlacklistRights = + MVideo & + Use<'VideoBlacklist', MVideoBlacklistUnfederated> + export type MVideoAccountLight = MVideo & Use<'VideoChannel', MChannelAccountLight>