Don't store remote rates of remote videos

In the future we'll stop to expose all available rates to improve users
privacy
pull/4867/head
Chocobozzz 2022-03-18 11:17:35 +01:00
parent 2e3f7a5a6f
commit 57e4e1c1a9
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
44 changed files with 402 additions and 338 deletions

View File

@ -8,6 +8,7 @@ if [ $# -eq 0 ]; then
fi
retries=3
speedFactor="${2:-1}"
runTest () {
jobname=$1
@ -53,7 +54,7 @@ elif [ "$1" = "client" ]; then
# Not in their own task, they need an index.html
pluginFiles="./dist/server/tests/plugins/html-injection.js ./dist/server/tests/api/server/plugins.js"
MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
elif [ "$1" = "cli-plugin" ]; then
npm run build:server
npm run setup:cli
@ -61,7 +62,7 @@ elif [ "$1" = "cli-plugin" ]; then
pluginsFiles=$(findTestFiles ./dist/server/tests/plugins html-injection.js)
cliFiles=$(findTestFiles ./dist/server/tests/cli)
MOCHA_PARALLEL=true runTest "$1" 2 $pluginsFiles
MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $pluginsFiles
runTest "$1" 1 $cliFiles
elif [ "$1" = "api-1" ]; then
npm run build:server
@ -70,7 +71,7 @@ elif [ "$1" = "api-1" ]; then
notificationsFiles=$(findTestFiles ./dist/server/tests/api/notifications)
searchFiles=$(findTestFiles ./dist/server/tests/api/search)
MOCHA_PARALLEL=true runTest "$1" 3 $notificationsFiles $searchFiles $checkParamFiles
MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $notificationsFiles $searchFiles $checkParamFiles
elif [ "$1" = "api-2" ]; then
npm run build:server
@ -78,13 +79,13 @@ elif [ "$1" = "api-2" ]; then
serverFiles=$(findTestFiles ./dist/server/tests/api/server plugins.js)
usersFiles=$(findTestFiles ./dist/server/tests/api/users)
MOCHA_PARALLEL=true runTest "$1" 3 $liveFiles $serverFiles $usersFiles
MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $liveFiles $serverFiles $usersFiles
elif [ "$1" = "api-3" ]; then
npm run build:server
videosFiles=$(findTestFiles ./dist/server/tests/api/videos)
MOCHA_PARALLEL=true runTest "$1" 3 $videosFiles
MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $videosFiles
elif [ "$1" = "api-4" ]; then
npm run build:server
@ -93,13 +94,13 @@ elif [ "$1" = "api-4" ]; then
objectStorageFiles=$(findTestFiles ./dist/server/tests/api/object-storage)
activitypubFiles=$(findTestFiles ./dist/server/tests/api/activitypub)
MOCHA_PARALLEL=true runTest "$1" 2 $moderationFiles $redundancyFiles $activitypubFiles $objectStorageFiles
MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $moderationFiles $redundancyFiles $activitypubFiles $objectStorageFiles
elif [ "$1" = "api-5" ]; then
npm run build:server
transcodingFiles=$(findTestFiles ./dist/server/tests/api/transcoding)
MOCHA_PARALLEL=true runTest "$1" 2 $transcodingFiles
MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $transcodingFiles
elif [ "$1" = "external-plugins" ]; then
npm run build:server

View File

@ -66,11 +66,13 @@ activityPubClientRouter.get('/accounts?/:name/playlists',
)
activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
executeIfActivityPub,
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
getAccountVideoRateFactory('like')
)
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
executeIfActivityPub,
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
getAccountVideoRateFactory('dislike')
)

View File

@ -134,8 +134,7 @@ async function searchVideoURI (url: string, res: express.Response) {
if (isUserAbleToSearchRemoteURI(res)) {
try {
const syncParam = {
likes: false,
dislikes: false,
rates: false,
shares: false,
comments: false,
thumbnail: true,

View File

@ -154,7 +154,9 @@ async function activityPubCollectionPagination (
id: baseUrl,
type: 'OrderedCollectionPage',
totalItems: result.total,
first: baseUrl + '?page=1'
first: result.data.length === 0
? undefined
: baseUrl + '?page=1'
}
}

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 690
const LAST_MIGRATION_VERSION = 695
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,28 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
const query = 'DELETE FROM "accountVideoRate" ' +
'WHERE "accountVideoRate".id IN (' +
'SELECT "accountVideoRate".id FROM "accountVideoRate" ' +
'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
'INNER JOIN actor ON actor.id = account."actorId" ' +
'INNER JOIN video ON video.id = "accountVideoRate"."videoId" ' +
'WHERE actor."serverId" IS NOT NULL AND video.remote IS TRUE' +
')'
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.BULKDELETE, transaction: utils.transaction })
}
function down () {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -1,68 +1,6 @@
import { Transaction } from 'sequelize'
import { ActivityAudience } from '../../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../../initializers/constants'
import { ActorModel } from '../../models/actor/actor'
import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share'
import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models'
function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
return {
to: [ accountActor.url ],
cc: actorsInvolvedInVideo.map(a => a.followersUrl)
}
}
function getVideoCommentAudience (
videoComment: MCommentOwnerVideo,
threadParentComments: MCommentOwner[],
actorsInvolvedInVideo: MActorFollowersUrl[],
isOrigin = false
): ActivityAudience {
const to = [ ACTIVITY_PUB.PUBLIC ]
const cc: string[] = []
// Owner of the video we comment
if (isOrigin === false) {
cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
}
// Followers of the poster
cc.push(videoComment.Account.Actor.followersUrl)
// Send to actors we reply to
for (const parentComment of threadParentComments) {
if (parentComment.isDeleted()) continue
cc.push(parentComment.Account.Actor.url)
}
return {
to,
cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
}
}
function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
return {
to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
cc: []
}
}
async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
const videoAll = video as VideoModel
const videoActor = videoAll.VideoChannel?.Account
? videoAll.VideoChannel.Account.Actor
: await ActorModel.loadFromAccountByVideoId(video.id, t)
actors.push(videoActor)
return actors
}
import { MActorFollowersUrl } from '../../types/models'
function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic)
@ -92,9 +30,5 @@ function audiencify<T> (object: T, audience: ActivityAudience) {
export {
buildAudience,
getAudience,
getRemoteVideoAudience,
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
audiencify,
getVideoCommentAudience
audiencify
}

View File

@ -2,7 +2,7 @@ import { ActivityAnnounce } from '../../../../shared/models/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { sequelizeTypescript } from '../../../initializers/database'
import { VideoShareModel } from '../../../models/video/video-share'
import { forwardVideoRelatedActivity } from '../send/utils'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { getOrCreateAPVideo } from '../videos'
import { Notifier } from '../../notifier'
import { logger } from '../../../helpers/logger'

View File

@ -9,7 +9,7 @@ import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFile
import { Notifier } from '../../notifier'
import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlists'
import { forwardVideoRelatedActivity } from '../send/utils'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { resolveThread } from '../video-comments'
import { getOrCreateAPVideo } from '../videos'
@ -55,7 +55,7 @@ export {
async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
const videoToCreateData = activity.object as VideoObject
const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video)

View File

@ -16,7 +16,7 @@ import {
MChannelActor,
MCommentOwnerVideo
} from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
const { activity, byActor } = options

View File

@ -1,11 +1,11 @@
import { VideoModel } from '@server/models/video/video'
import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { sequelizeTypescript } from '../../../initializers/database'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateAPVideo } from '../videos'
import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
const { activity, byActor } = options
@ -29,16 +29,23 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject })
const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
// We don't care about dislikes of remote videos
if (!onlyVideo.isOwned()) return
return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
if (existingRate && existingRate.type === 'dislike') return
await video.increment('dislikes', { transaction: t })
video.dislikes++
if (existingRate && existingRate.type === 'like') {
await video.decrement('likes', { transaction: t })
video.likes--
}
const rate = existingRate || new AccountVideoRateModel()
@ -49,11 +56,6 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
await rate.save({ transaction: t })
if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
await federateVideoIfNeeded(video, false, t)
})
}

View File

@ -1,3 +1,4 @@
import { VideoModel } from '@server/models/video/video'
import { ActivityLike } from '../../../../shared/models/activitypub'
import { getAPId } from '../../../helpers/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
@ -5,11 +6,11 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateAPVideo } from '../videos'
import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
const { activity, byActor } = options
return retryTransactionWrapper(processLikeVideo, byActor, activity)
}
@ -27,17 +28,24 @@ 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 } = await getOrCreateAPVideo({ videoObject: videoUrl })
const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
// We don't care about likes of remote videos
if (!onlyVideo.isOwned()) return
return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
if (existingRate && existingRate.type === 'like') return
if (existingRate && existingRate.type === 'dislike') {
await video.decrement('dislikes', { transaction: t })
video.dislikes--
}
await video.increment('likes', { transaction: t })
video.likes++
const rate = existingRate || new AccountVideoRateModel()
rate.type = 'like'
@ -47,11 +55,6 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
await rate.save({ transaction: t })
if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
await federateVideoIfNeeded(video, false, t)
})
}

View File

@ -1,3 +1,4 @@
import { VideoModel } from '@server/models/video/video'
import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub'
import { DislikeObject } from '../../../../shared/models/activitypub/objects'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
@ -10,8 +11,8 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc
import { VideoShareModel } from '../../../models/video/video-share'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateAPVideo } from '../videos'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
const { activity, byActor } = options
@ -55,23 +56,22 @@ export {
async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
const likeActivity = activity.object as ActivityLike
const { video } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
// We don't care about likes of remote videos
if (!onlyVideo.isOwned()) return
return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t)
if (!rate || rate.type !== 'like') throw new Error(`Unknown like by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })
await video.decrement('likes', { transaction: t })
if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
video.likes--
await federateVideoIfNeeded(video, false, t)
})
}
@ -80,26 +80,27 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
? activity.object
: activity.object.object as DislikeObject
const { video } = await getOrCreateAPVideo({ videoObject: dislike.object })
const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object })
// We don't care about likes of remote videos
if (!onlyVideo.isOwned()) return
return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t)
if (!rate || rate.type !== 'dislike') throw new Error(`Unknown dislike by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })
await video.decrement('dislikes', { transaction: t })
video.dislikes--
if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
await federateVideoIfNeeded(video, false, t)
})
}
// ---------------------------------------------------------------------------
async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
const cacheFileObject = activity.object.object as CacheFileObject
@ -125,19 +126,6 @@ async function processUndoCacheFile (byActor: MActorSignature, activity: Activit
})
}
function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
return sequelizeTypescript.transaction(async t => {
const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
await actorFollow.destroy({ transaction: t })
return undefined
})
}
function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
return sequelizeTypescript.transaction(async t => {
const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
@ -155,3 +143,18 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
}
})
}
// ---------------------------------------------------------------------------
function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
return sequelizeTypescript.transaction(async t => {
const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
await actorFollow.destroy({ transaction: t })
return undefined
})
}

View File

@ -13,7 +13,7 @@ import { MActorFull, MActorSignature } from '../../../types/models'
import { APActorUpdater } from '../actors/updater'
import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlists'
import { forwardVideoRelatedActivity } from '../send/utils'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {

View File

@ -2,7 +2,7 @@ import { VideoViews } from '@server/lib/video-views'
import { ActivityView } from '../../../../shared/models/activitypub'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { getOrCreateAPVideo } from '../videos'
async function processViewActivity (options: APProcessorOptions<ActivityView>) {

View File

@ -1,9 +1,9 @@
import { ActivityAccept, ActivityFollow } from '../../../../shared/models/activitypub'
import { ActivityAccept, ActivityFollow } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MActor, MActorFollowActors } from '../../../types/models'
import { getLocalActorFollowAcceptActivityPubUrl } from '../url'
import { buildFollowActivity } from './send-follow'
import { unicastTo } from './utils'
import { unicastTo } from './shared/send-utils'
function sendAccept (actorFollow: MActorFollowActors) {
const follower = actorFollow.ActorFollower

View File

@ -1,10 +1,11 @@
import { Transaction } from 'sequelize'
import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub'
import { broadcastToFollowers } from './utils'
import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience'
import { ActivityAnnounce, ActivityAudience } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MActorLight, MVideo } from '../../../types/models'
import { MVideoShare } from '../../../types/models/video'
import { audiencify, getAudience } from '../audience'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf } from './shared'
import { broadcastToFollowers } from './shared/send-utils'
async function buildAnnounceWithVideoAudience (
byActor: MActorLight,

View File

@ -1,11 +1,8 @@
import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
import { getServerActor } from '@server/models/application/application'
import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
import { logger, loggerTagsFactory } from '../../../helpers/logger'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoCommentModel } from '../../../models/video/video-comment'
import {
MActorLight,
MCommentOwnerVideo,
@ -15,8 +12,16 @@ import {
MVideoRedundancyFileVideo,
MVideoRedundancyStreamingPlaylistVideo
} from '../../../types/models'
import { getServerActor } from '@server/models/application/application'
import { ContextType } from '@shared/models/activitypub/context'
import { audiencify, getAudience } from '../audience'
import {
broadcastToActors,
broadcastToFollowers,
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
getVideoCommentAudience,
sendVideoRelatedActivity,
unicastTo
} from './shared'
const lTags = loggerTagsFactory('ap', 'create')

View File

@ -1,15 +1,16 @@
import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application'
import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
import { ActivityAudience, ActivityDelete } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share'
import { MActorUrl } from '../../../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video'
import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
import { audiencify } from '../audience'
import { getDeleteActivityPubUrl } from '../url'
import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
import { getActorsInvolvedInVideo, getVideoCommentAudience } from './shared'
import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
logger.info('Creating job to broadcast delete of video %s.', video.url)

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize'
import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
import { ActivityAudience, ActivityDislike } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
import { sendVideoRelatedActivity } from './utils'
import { audiencify, getAudience } from '../audience'
import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
import { audiencify, getAudience } from '../audience'
import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
import { sendVideoActivityToOrigin } from './shared/send-utils'
function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to dislike %s.', video.url)
@ -15,7 +15,7 @@ function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction
return buildDislikeActivity(url, byActor, video, audience)
}
return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
}
function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
import { ActivityAudience, ActivityFlag } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
import { audiencify, getAudience } from '../audience'
import { getLocalAbuseActivityPubUrl } from '../url'
import { unicastTo } from './utils'
import { unicastTo } from './shared/send-utils'
function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
if (!flaggedAccount.Actor.serverId) return // Local user

View File

@ -1,8 +1,8 @@
import { Transaction } from 'sequelize'
import { ActivityFollow } from '../../../../shared/models/activitypub'
import { ActivityFollow } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MActor, MActorFollowActors } from '../../../types/models'
import { unicastTo } from './utils'
import { unicastTo } from './shared/send-utils'
function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
const me = actorFollow.ActorFollower

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub'
import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
import { sendVideoRelatedActivity } from './utils'
import { audiencify, getAudience } from '../audience'
import { ActivityAudience, ActivityLike } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
import { audiencify, getAudience } from '../audience'
import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
import { sendVideoActivityToOrigin } from './shared/send-utils'
function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to like %s.', video.url)
@ -15,7 +15,7 @@ function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
return buildLikeActivity(url, byActor, video, audience)
}
return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
}
function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike {

View File

@ -1,9 +1,9 @@
import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
import { ActivityFollow, ActivityReject } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { MActor } from '../../../types/models'
import { getLocalActorFollowRejectActivityPubUrl } from '../url'
import { buildFollowActivity } from './send-follow'
import { unicastTo } from './utils'
import { unicastTo } from './shared/send-utils'
function sendReject (followUrl: string, follower: MActor, following: MActor) {
if (!follower.serverId) { // This should never happen

View File

@ -7,7 +7,7 @@ import {
ActivityFollow,
ActivityLike,
ActivityUndo
} from '../../../../shared/models/activitypub'
} from '@shared/models'
import { logger } from '../../../helpers/logger'
import { VideoModel } from '../../../models/video/video'
import {
@ -27,7 +27,7 @@ import { buildCreateActivity } from './send-create'
import { buildDislikeActivity } from './send-dislike'
import { buildFollowActivity } from './send-follow'
import { buildLikeActivity } from './send-like'
import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
import { broadcastToFollowers, sendVideoActivityToOrigin, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
const me = actorFollow.ActorFollower
@ -46,6 +46,8 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
}
// ---------------------------------------------------------------------------
async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
logger.info('Creating job to undo announce %s.', videoShare.url)
@ -58,24 +60,6 @@ async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException)
}
async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to undo a like of video %s.', video.url)
const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
const likeActivity = buildLikeActivity(likeUrl, byActor, video)
return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
}
async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to undo a dislike of video %s.', video.url)
const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
}
async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
logger.info('Creating job to undo cache file %s.', redundancyModel.url)
@ -93,6 +77,26 @@ async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedund
// ---------------------------------------------------------------------------
async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to undo a like of video %s.', video.url)
const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
const likeActivity = buildLikeActivity(likeUrl, byActor, video)
return sendUndoVideoToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
}
async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to undo a dislike of video %s.', video.url)
const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
return sendUndoVideoToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
}
// ---------------------------------------------------------------------------
export {
sendUndoFollow,
sendUndoLike,
@ -126,7 +130,7 @@ async function sendUndoVideoRelatedActivity (options: {
byActor: MActor
video: MVideoAccountLight
url: string
activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
activity: ActivityFollow | ActivityCreate | ActivityAnnounce
transaction: Transaction
}) {
const activityBuilder = (audience: ActivityAudience) => {
@ -137,3 +141,19 @@ async function sendUndoVideoRelatedActivity (options: {
return sendVideoRelatedActivity(activityBuilder, options)
}
async function sendUndoVideoToOriginActivity (options: {
byActor: MActor
video: MVideoAccountLight
url: string
activity: ActivityLike | ActivityDislike
transaction: Transaction
}) {
const activityBuilder = (audience: ActivityAudience) => {
const undoUrl = getUndoActivityPubUrl(options.url)
return undoActivityData(undoUrl, options.byActor, options.activity, audience)
}
return sendVideoActivityToOrigin(activityBuilder, options)
}

View File

@ -1,14 +1,10 @@
import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { getServerActor } from '@server/models/application/application'
import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video'
import { VideoShareModel } from '../../../models/video/video-share'
import { getUpdateActivityPubUrl } from '../url'
import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
import { logger } from '../../../helpers/logger'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import {
MAccountDefault,
MActor,
@ -19,7 +15,10 @@ import {
MVideoPlaylistFull,
MVideoRedundancyVideo
} from '../../../types/models'
import { getServerActor } from '@server/models/application/application'
import { audiencify, getAudience } from '../audience'
import { getUpdateActivityPubUrl } from '../url'
import { getActorsInvolvedInVideo } from './shared'
import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils'
async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
const video = videoArg as MVideoAP

View File

@ -1,12 +1,12 @@
import { Transaction } from 'sequelize'
import { VideoViews } from '@server/lib/video-views'
import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models'
import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
import { ActivityAudience, ActivityView } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor'
import { audiencify, getAudience } from '../audience'
import { getLocalVideoViewActivityPubUrl } from '../url'
import { sendVideoRelatedActivity } from './utils'
import { sendVideoRelatedActivity } from './shared/send-utils'
async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
logger.info('Creating job to send view of %s.', video.url)

View File

@ -0,0 +1,74 @@
import { Transaction } from 'sequelize/dist'
import { ACTIVITY_PUB } from '@server/initializers/constants'
import { ActorModel } from '@server/models/actor/actor'
import { VideoModel } from '@server/models/video/video'
import { VideoShareModel } from '@server/models/video/video-share'
import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models'
import { ActivityAudience } from '@shared/models'
function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
return {
to: [ accountActor.url ],
cc: actorsInvolvedInVideo.map(a => a.followersUrl)
}
}
function getVideoCommentAudience (
videoComment: MCommentOwnerVideo,
threadParentComments: MCommentOwner[],
actorsInvolvedInVideo: MActorFollowersUrl[],
isOrigin = false
): ActivityAudience {
const to = [ ACTIVITY_PUB.PUBLIC ]
const cc: string[] = []
// Owner of the video we comment
if (isOrigin === false) {
cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
}
// Followers of the poster
cc.push(videoComment.Account.Actor.followersUrl)
// Send to actors we reply to
for (const parentComment of threadParentComments) {
if (parentComment.isDeleted()) continue
cc.push(parentComment.Account.Actor.url)
}
return {
to,
cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
}
}
function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
return {
to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
cc: []
}
}
async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
const videoAll = video as VideoModel
const videoActor = videoAll.VideoChannel?.Account
? videoAll.VideoChannel.Account.Actor
: await ActorModel.loadFromAccountByVideoId(video.id, t)
actors.push(videoActor)
return actors
}
// ---------------------------------------------------------------------------
export {
getOriginVideoAudience,
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
getVideoCommentAudience
}

View File

@ -0,0 +1,2 @@
export * from './audience-utils'
export * from './send-utils'

View File

@ -1,15 +1,15 @@
import { Transaction } from 'sequelize'
import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache'
import { getServerActor } from '@server/models/application/application'
import { Activity, ActivityAudience } from '@shared/models'
import { ContextType } from '@shared/models/activitypub/context'
import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
import { afterCommitIfTransaction } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor'
import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
import { JobQueue } from '../../job-queue'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
import { afterCommitIfTransaction } from '../../../../helpers/database-utils'
import { logger } from '../../../../helpers/logger'
import { ActorModel } from '../../../../models/actor/actor'
import { ActorFollowModel } from '../../../../models/actor/actor-follow'
import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../../types/models'
import { JobQueue } from '../../../job-queue'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAudience } from './audience-utils'
async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
byActor: MActorLight
@ -23,16 +23,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
// Send to origin
if (video.isOwned() === false) {
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
const activity = activityBuilder(audience)
return afterCommitIfTransaction(transaction, () => {
return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
})
return sendVideoActivityToOrigin(activityBuilder, options)
}
// Send to followers
@ -44,6 +35,30 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
}
async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAudience) => Activity, options: {
byActor: MActorLight
video: MVideoImmutable | MVideoAccountLight
actorsInvolvedInVideo?: MActorLight[]
transaction?: Transaction
contextType?: ContextType
}) {
const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url)
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
const audience = getOriginVideoAudience(accountActor, actorsInvolvedInVideo)
const activity = activityBuilder(audience)
return afterCommitIfTransaction(transaction, () => {
return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
})
}
// ---------------------------------------------------------------------------
async function forwardVideoRelatedActivity (
activity: Activity,
t: Transaction,
@ -92,6 +107,8 @@ async function forwardActivity (
return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }))
}
// ---------------------------------------------------------------------------
async function broadcastToFollowers (
data: any,
byActor: MActorId,
@ -177,6 +194,7 @@ export {
unicastTo,
forwardActivity,
broadcastToActors,
sendVideoActivityToOrigin,
forwardVideoRelatedActivity,
sendVideoRelatedActivity
}

View File

@ -87,7 +87,7 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
// Maybe it's a reply to a video?
// If yes, it's done: we resolved all the thread
const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
const syncParam = { rates: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !video.hasPrivacyForFederation()) {

View File

@ -1,36 +1,48 @@
import { map } from 'bluebird'
import { Transaction } from 'sequelize'
import { doJSONRequest } from '@server/helpers/requests'
import { VideoRateType } from '../../../shared/models/videos'
import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
import { logger, loggerTagsFactory } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
import { getOrCreateAPActor } from './actors'
import { MAccountActor, MActorUrl, MVideoAccountLight, MVideoFullLight, MVideoId } from '../../types/models'
import { sendLike, sendUndoDislike, sendUndoLike } from './send'
import { sendDislike } from './send/send-dislike'
import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
const lTags = loggerTagsFactory('ap', 'video-rate', 'create')
async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
await map(ratesUrl, async rateUrl => {
try {
await createRate(rateUrl, video, rate)
} catch (err) {
logger.info('Cannot add rate %s.', rateUrl, { err, ...lTags(rateUrl, video.uuid, video.url) })
}
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
}
import { federateVideoIfNeeded } from './videos'
async function sendVideoRateChange (
account: MAccountActor,
video: MVideoFullLight,
likes: number,
dislikes: number,
t: Transaction
) {
if (video.isOwned()) return federateVideoIfNeeded(video, false, t)
return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
}
function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
return rateType === 'like'
? getVideoLikeActivityPubUrlByLocalActor(actor, video)
: getVideoDislikeActivityPubUrlByLocalActor(actor, video)
}
// ---------------------------------------------------------------------------
export {
getLocalRateUrl,
sendVideoRateChange
}
// ---------------------------------------------------------------------------
async function sendVideoRateChangeToOrigin (
account: MAccountActor,
video: MVideoAccountLight,
likes: number,
dislikes: number,
t: Transaction
) {
// Local video, we don't need to send like
if (video.isOwned()) return
const actor = account.Actor
// Keep the order: first we undo and then we create
@ -45,46 +57,3 @@ async function sendVideoRateChange (
// Dislike
if (dislikes > 0) await sendDislike(actor, video, t)
}
function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
return rateType === 'like'
? getVideoLikeActivityPubUrlByLocalActor(actor, video)
: getVideoDislikeActivityPubUrlByLocalActor(actor, video)
}
// ---------------------------------------------------------------------------
export {
getLocalRateUrl,
createRates,
sendVideoRateChange
}
// ---------------------------------------------------------------------------
async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) {
// Fetch url
const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getAPId(body.actor)
if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
}
if (checkUrlsSameHost(body.id, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateAPActor(actorUrl)
const entry = {
videoId: video.id,
accountId: actor.Account.id,
type: rate,
url: body.id
}
// Video "likes"/"dislikes" will be updated by the caller
await AccountVideoRateModel.upsert(entry)
}

View File

@ -42,7 +42,7 @@ async function getOrCreateAPVideo (
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
// Default params
const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
const syncParam = options.syncParam || { rates: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
const fetchType = options.fetchType || 'all'
const allowRefresh = options.allowRefresh !== false

View File

@ -1,20 +1,20 @@
import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { JobQueue } from '@server/lib/job-queue'
import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
import { VideoModel } from '@server/models/video/video'
import { VideoCommentModel } from '@server/models/video/video-comment'
import { VideoShareModel } from '@server/models/video/video-share'
import { MVideo } from '@server/types/models'
import { ActivitypubHttpFetcherPayload, VideoObject } from '@shared/models'
import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
import { crawlCollectionPage } from '../../crawl'
import { addVideoShares } from '../../share'
import { addVideoComments } from '../../video-comments'
import { createRates } from '../../video-rates'
const lTags = loggerTagsFactory('ap', 'video')
type SyncParam = {
likes: boolean
dislikes: boolean
rates: boolean
shares: boolean
comments: boolean
thumbnail: boolean
@ -24,45 +24,57 @@ type SyncParam = {
async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) {
logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
await syncRates('like', video, fetchedVideo, syncParam.likes)
await syncRates('dislike', video, fetchedVideo, syncParam.dislikes)
const ratePromise = updateVideoRates(video, fetchedVideo)
if (syncParam.rates) await ratePromise
await syncShares(video, fetchedVideo, syncParam.shares)
await syncComments(video, fetchedVideo, syncParam.comments)
}
async function updateVideoRates (video: MVideo, fetchedVideo: VideoObject) {
const [ likes, dislikes ] = await Promise.all([
getRatesCount('like', video, fetchedVideo),
getRatesCount('dislike', video, fetchedVideo)
])
return runInReadCommittedTransaction(async t => {
await VideoModel.updateRatesOf(video.id, 'like', likes, t)
await VideoModel.updateRatesOf(video.id, 'dislike', dislikes, t)
})
}
// ---------------------------------------------------------------------------
export {
SyncParam,
syncVideoExternalAttributes
syncVideoExternalAttributes,
updateVideoRates
}
// ---------------------------------------------------------------------------
function createJob (payload: ActivitypubHttpFetcherPayload) {
return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
}
function syncRates (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
const uri = type === 'like'
? fetchedVideo.likes
: fetchedVideo.dislikes
if (!isSync) {
const jobType = type === 'like'
? 'video-likes'
: 'video-dislikes'
logger.info('Sync %s of video %s', type, video.url)
const options = { activityPub: true }
return createJob({ uri, videoId: video.id, type: jobType })
const response = await doJSONRequest<ActivityPubOrderedCollection<any>>(uri, options)
const totalItems = response.body.totalItems
if (isNaN(totalItems)) {
logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body: response.body })
return
}
const handler = items => createRates(items, video, type)
const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, type, crawlStartDate)
return totalItems
}
return crawlCollectionPage<string>(uri, handler, cleaner)
.catch(err => logger.error('Cannot add rate of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
function createJob (payload: ActivitypubHttpFetcherPayload) {
return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
}
function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {

View File

@ -7,7 +7,7 @@ import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
import { VideoLiveModel } from '@server/models/video/video-live'
import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
import { VideoObject, VideoPrivacy } from '@shared/models'
import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared'
import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
export class APVideoUpdater extends APVideoAbstractBuilder {
private readonly wasPrivateVideo: boolean
@ -74,6 +74,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
transaction: undefined
})
await updateVideoRates(videoUpdated, this.videoObject)
// Notify our users?
if (this.wasPrivateVideo || this.wasUnlistedVideo) {
Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)

View File

@ -34,7 +34,7 @@ async function processActivityPubCleaner (_job: Job) {
if (result?.status === 'deleted') {
const { videoId, type } = result.data
await VideoModel.updateRatesOf(videoId, type, undefined)
await VideoModel.syncLocalRates(videoId, type, undefined)
}
}, { concurrency: AP_CLEANER.CONCURRENCY })
}

View File

@ -1,7 +1,6 @@
import { Job } from 'bull'
import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoModel } from '../../../models/video/video'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share'
@ -11,7 +10,6 @@ import { createAccountPlaylists } from '../../activitypub/playlists'
import { processActivities } from '../../activitypub/process'
import { addVideoShares } from '../../activitypub/share'
import { addVideoComments } from '../../activitypub/video-comments'
import { createRates } from '../../activitypub/video-rates'
async function processActivityPubHttpFetcher (job: Job) {
logger.info('Processing ActivityPub fetcher in job %d.', job.id)
@ -23,16 +21,12 @@ async function processActivityPubHttpFetcher (job: Job) {
const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }),
'video-likes': items => createRates(items, video, 'like'),
'video-dislikes': items => createRates(items, video, 'dislike'),
'video-shares': items => addVideoShares(items, video),
'video-comments': items => addVideoComments(items),
'account-playlists': items => createAccountPlaylists(items)
}
const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = {
'video-likes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate),
'video-dislikes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate),
'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
}

View File

@ -28,7 +28,7 @@ export {
async function refreshVideo (videoUrl: string) {
const fetchType = 'all' as 'all'
const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
const syncParam = { rates: true, shares: true, comments: true, thumbnail: true }
const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
if (videoFromDatabase) {

View File

@ -352,7 +352,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
// We need more attributes and check if the video still exists
const getVideoOptions = {
videoObject: videoUrl,
syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
syncParam: { rates: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
fetchType: 'all' as 'all'
}
const { video } = await getOrCreateAPVideo(getVideoOptions)

View File

@ -12,7 +12,7 @@ import { AttributesOnly } from '@shared/typescript-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
import { ActorModel } from '../actor/actor'
import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
import { AccountModel } from './account'
@ -249,28 +249,6 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
]).then(([ total, data ]) => ({ total, data }))
}
static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
return AccountVideoRateModel.sequelize.transaction(async t => {
const query = {
where: {
updatedAt: {
[Op.lt]: beforeUpdatedAt
},
videoId,
type,
accountId: {
[Op.notIn]: buildLocalAccountIdsIn()
}
},
transaction: t
}
await AccountVideoRateModel.destroy(query)
return VideoModel.updateRatesOf(videoId, type, t)
})
}
toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
return {
video: this.Video.toFormattedJSON(),

View File

@ -1402,7 +1402,21 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
})
}
static updateRatesOf (videoId: number, type: VideoRateType, t: Transaction) {
static updateRatesOf (videoId: number, type: VideoRateType, count: number, t: Transaction) {
const field = type === 'like'
? 'likes'
: 'dislikes'
const rawQuery = `UPDATE "video" SET "${field}" = :count WHERE "video"."id" = :videoId`
return AccountVideoRateModel.sequelize.query(rawQuery, {
transaction: t,
replacements: { videoId, rateType: type, count },
type: QueryTypes.UPDATE
})
}
static syncLocalRates (videoId: number, type: VideoRateType, t: Transaction) {
const field = type === 'like'
? 'likes'
: 'dislikes'

View File

@ -606,8 +606,8 @@ describe('Test multiple servers', function () {
for (const baseVideo of baseVideos) {
const sameVideo = data.find(video => video.name === baseVideo.name)
expect(baseVideo.likes).to.equal(sameVideo.likes)
expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`)
}
}
})

View File

@ -1,8 +1,9 @@
export * from './objects'
export * from './activity'
export * from './activitypub-actor'
export * from './activitypub-collection'
export * from './activitypub-ordered-collection'
export * from './activitypub-root'
export * from './activitypub-signature'
export * from './objects'
export * from './context'
export * from './webfinger'

View File

@ -52,7 +52,7 @@ export type ActivitypubFollowPayload = {
assertIsChannel?: boolean
}
export type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
export type FetchType = 'activity' | 'video-shares' | 'video-comments' | 'account-playlists'
export type ActivitypubHttpFetcherPayload = {
uri: string
type: FetchType