Don't store remote rates of remote videos

In the future we'll stop to expose all available rates to improve users
privacy
This commit is contained in:
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 fi
retries=3 retries=3
speedFactor="${2:-1}"
runTest () { runTest () {
jobname=$1 jobname=$1
@ -53,7 +54,7 @@ elif [ "$1" = "client" ]; then
# Not in their own task, they need an index.html # 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" 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 elif [ "$1" = "cli-plugin" ]; then
npm run build:server npm run build:server
npm run setup:cli npm run setup:cli
@ -61,7 +62,7 @@ elif [ "$1" = "cli-plugin" ]; then
pluginsFiles=$(findTestFiles ./dist/server/tests/plugins html-injection.js) pluginsFiles=$(findTestFiles ./dist/server/tests/plugins html-injection.js)
cliFiles=$(findTestFiles ./dist/server/tests/cli) 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 runTest "$1" 1 $cliFiles
elif [ "$1" = "api-1" ]; then elif [ "$1" = "api-1" ]; then
npm run build:server npm run build:server
@ -70,7 +71,7 @@ elif [ "$1" = "api-1" ]; then
notificationsFiles=$(findTestFiles ./dist/server/tests/api/notifications) notificationsFiles=$(findTestFiles ./dist/server/tests/api/notifications)
searchFiles=$(findTestFiles ./dist/server/tests/api/search) 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 elif [ "$1" = "api-2" ]; then
npm run build:server npm run build:server
@ -78,13 +79,13 @@ elif [ "$1" = "api-2" ]; then
serverFiles=$(findTestFiles ./dist/server/tests/api/server plugins.js) serverFiles=$(findTestFiles ./dist/server/tests/api/server plugins.js)
usersFiles=$(findTestFiles ./dist/server/tests/api/users) 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 elif [ "$1" = "api-3" ]; then
npm run build:server npm run build:server
videosFiles=$(findTestFiles ./dist/server/tests/api/videos) 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 elif [ "$1" = "api-4" ]; then
npm run build:server npm run build:server
@ -93,13 +94,13 @@ elif [ "$1" = "api-4" ]; then
objectStorageFiles=$(findTestFiles ./dist/server/tests/api/object-storage) objectStorageFiles=$(findTestFiles ./dist/server/tests/api/object-storage)
activitypubFiles=$(findTestFiles ./dist/server/tests/api/activitypub) 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 elif [ "$1" = "api-5" ]; then
npm run build:server npm run build:server
transcodingFiles=$(findTestFiles ./dist/server/tests/api/transcoding) 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 elif [ "$1" = "external-plugins" ]; then
npm run build:server npm run build:server

View File

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

View File

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

View File

@ -154,7 +154,9 @@ async function activityPubCollectionPagination (
id: baseUrl, id: baseUrl,
type: 'OrderedCollectionPage', type: 'OrderedCollectionPage',
totalItems: result.total, 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 { ActivityAudience } from '../../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../../initializers/constants' import { ACTIVITY_PUB } from '../../initializers/constants'
import { ActorModel } from '../../models/actor/actor' import { MActorFollowersUrl } from '../../types/models'
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
}
function getAudience (actorSender: MActorFollowersUrl, isPublic = true) { function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic) return buildAudience([ actorSender.followersUrl ], isPublic)
@ -92,9 +30,5 @@ function audiencify<T> (object: T, audience: ActivityAudience) {
export { export {
buildAudience, buildAudience,
getAudience, getAudience,
getRemoteVideoAudience, audiencify
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
audiencify,
getVideoCommentAudience
} }

View File

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

View File

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

View File

@ -16,7 +16,7 @@ import {
MChannelActor, MChannelActor,
MCommentOwnerVideo MCommentOwnerVideo
} from '../../../types/models' } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
const { activity, byActor } = options 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 { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
import { getOrCreateAPVideo } from '../videos'
async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
const { activity, byActor } = options 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) 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 => { 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) const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
if (existingRate && existingRate.type === 'dislike') return if (existingRate && existingRate.type === 'dislike') return
await video.increment('dislikes', { transaction: t }) await video.increment('dislikes', { transaction: t })
video.dislikes++
if (existingRate && existingRate.type === 'like') { if (existingRate && existingRate.type === 'like') {
await video.decrement('likes', { transaction: t }) await video.decrement('likes', { transaction: t })
video.likes--
} }
const rate = existingRate || new AccountVideoRateModel() const rate = existingRate || new AccountVideoRateModel()
@ -49,11 +56,6 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
await rate.save({ transaction: t }) await rate.save({ transaction: t })
if (video.isOwned()) { await federateVideoIfNeeded(video, false, t)
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
}) })
} }

View File

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

View File

@ -1,3 +1,4 @@
import { VideoModel } from '@server/models/video/video'
import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub'
import { DislikeObject } from '../../../../shared/models/activitypub/objects' import { DislikeObject } from '../../../../shared/models/activitypub/objects'
import { retryTransactionWrapper } from '../../../helpers/database-utils' 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 { VideoShareModel } from '../../../models/video/video-share'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { getOrCreateAPVideo } from '../videos' import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -55,23 +56,22 @@ export {
async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
const likeActivity = activity.object as ActivityLike 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 => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 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) 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}.`) 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 rate.destroy({ transaction: t })
await video.decrement('likes', { transaction: t }) await video.decrement('likes', { transaction: t })
if (video.isOwned()) { video.likes--
// Don't resend the activity to the sender await federateVideoIfNeeded(video, false, t)
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
}) })
} }
@ -80,26 +80,27 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
? activity.object ? activity.object
: activity.object.object as DislikeObject : 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 => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 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) 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}.`) 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 rate.destroy({ transaction: t })
await video.decrement('dislikes', { transaction: t }) await video.decrement('dislikes', { transaction: t })
video.dislikes--
if (video.isOwned()) { await federateVideoIfNeeded(video, false, t)
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, video)
}
}) })
} }
// ---------------------------------------------------------------------------
async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
const cacheFileObject = activity.object.object as CacheFileObject 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) { function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const share = await VideoShareModel.loadByUrl(announceActivity.id, 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 { APActorUpdater } from '../actors/updater'
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlists' import { createOrUpdateVideoPlaylist } from '../playlists'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { APVideoUpdater, getOrCreateAPVideo } from '../videos' import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 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 { ActivityView } from '../../../../shared/models/activitypub'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { getOrCreateAPVideo } from '../videos' import { getOrCreateAPVideo } from '../videos'
async function processViewActivity (options: APProcessorOptions<ActivityView>) { 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 { logger } from '../../../helpers/logger'
import { MActor, MActorFollowActors } from '../../../types/models' import { MActor, MActorFollowActors } from '../../../types/models'
import { getLocalActorFollowAcceptActivityPubUrl } from '../url' import { getLocalActorFollowAcceptActivityPubUrl } from '../url'
import { buildFollowActivity } from './send-follow' import { buildFollowActivity } from './send-follow'
import { unicastTo } from './utils' import { unicastTo } from './shared/send-utils'
function sendAccept (actorFollow: MActorFollowActors) { function sendAccept (actorFollow: MActorFollowActors) {
const follower = actorFollow.ActorFollower const follower = actorFollow.ActorFollower

View File

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

View File

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

View File

@ -1,15 +1,16 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application' 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 { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor' import { ActorModel } from '../../../models/actor/actor'
import { VideoCommentModel } from '../../../models/video/video-comment' import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { MActorUrl } from '../../../types/models' import { MActorUrl } from '../../../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video' import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video'
import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' import { audiencify } from '../audience'
import { getDeleteActivityPubUrl } from '../url' 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) { async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
logger.info('Creating job to broadcast delete of video %s.', video.url) logger.info('Creating job to broadcast delete of video %s.', video.url)

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { getVideoDislikeActivityPubUrlByLocalActor } from '../url' import { ActivityAudience, ActivityDislike } from '@shared/models'
import { logger } from '../../../helpers/logger' 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 { 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) { function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to dislike %s.', video.url) 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 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 { function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub' import { ActivityAudience, ActivityLike } from '@shared/models'
import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
import { sendVideoRelatedActivity } from './utils'
import { audiencify, getAudience } from '../audience'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models' 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) { function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
logger.info('Creating job to like %s.', video.url) 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 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 { 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 { logger } from '../../../helpers/logger'
import { MActor } from '../../../types/models' import { MActor } from '../../../types/models'
import { getLocalActorFollowRejectActivityPubUrl } from '../url' import { getLocalActorFollowRejectActivityPubUrl } from '../url'
import { buildFollowActivity } from './send-follow' import { buildFollowActivity } from './send-follow'
import { unicastTo } from './utils' import { unicastTo } from './shared/send-utils'
function sendReject (followUrl: string, follower: MActor, following: MActor) { function sendReject (followUrl: string, follower: MActor, following: MActor) {
if (!follower.serverId) { // This should never happen if (!follower.serverId) { // This should never happen

View File

@ -7,7 +7,7 @@ import {
ActivityFollow, ActivityFollow,
ActivityLike, ActivityLike,
ActivityUndo ActivityUndo
} from '../../../../shared/models/activitypub' } from '@shared/models'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { import {
@ -27,7 +27,7 @@ import { buildCreateActivity } from './send-create'
import { buildDislikeActivity } from './send-dislike' import { buildDislikeActivity } from './send-dislike'
import { buildFollowActivity } from './send-follow' import { buildFollowActivity } from './send-follow'
import { buildLikeActivity } from './send-like' 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) { function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
const me = actorFollow.ActorFollower const me = actorFollow.ActorFollower
@ -46,6 +46,8 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl)) t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
} }
// ---------------------------------------------------------------------------
async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) { async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
logger.info('Creating job to undo announce %s.', videoShare.url) 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) 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) { async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
logger.info('Creating job to undo cache file %s.', redundancyModel.url) 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 { export {
sendUndoFollow, sendUndoFollow,
sendUndoLike, sendUndoLike,
@ -126,7 +130,7 @@ async function sendUndoVideoRelatedActivity (options: {
byActor: MActor byActor: MActor
video: MVideoAccountLight video: MVideoAccountLight
url: string url: string
activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce activity: ActivityFollow | ActivityCreate | ActivityAnnounce
transaction: Transaction transaction: Transaction
}) { }) {
const activityBuilder = (audience: ActivityAudience) => { const activityBuilder = (audience: ActivityAudience) => {
@ -137,3 +141,19 @@ async function sendUndoVideoRelatedActivity (options: {
return sendVideoRelatedActivity(activityBuilder, 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 { Transaction } from 'sequelize'
import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' import { getServerActor } from '@server/models/application/application'
import { VideoPrivacy } from '../../../../shared/models/videos' import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoShareModel } from '../../../models/video/video-share' 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 { import {
MAccountDefault, MAccountDefault,
MActor, MActor,
@ -19,7 +15,10 @@ import {
MVideoPlaylistFull, MVideoPlaylistFull,
MVideoRedundancyVideo MVideoRedundancyVideo
} from '../../../types/models' } 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) { async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
const video = videoArg as MVideoAP const video = videoArg as MVideoAP

View File

@ -1,12 +1,12 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { VideoViews } from '@server/lib/video-views' import { VideoViews } from '@server/lib/video-views'
import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' 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 { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor' import { ActorModel } from '../../../models/actor/actor'
import { audiencify, getAudience } from '../audience' import { audiencify, getAudience } from '../audience'
import { getLocalVideoViewActivityPubUrl } from '../url' import { getLocalVideoViewActivityPubUrl } from '../url'
import { sendVideoRelatedActivity } from './utils' import { sendVideoRelatedActivity } from './shared/send-utils'
async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) { async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
logger.info('Creating job to send view of %s.', video.url) 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 { Transaction } from 'sequelize'
import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache' import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { Activity, ActivityAudience } from '@shared/models'
import { ContextType } from '@shared/models/activitypub/context' import { ContextType } from '@shared/models/activitypub/context'
import { Activity, ActivityAudience } from '../../../../shared/models/activitypub' import { afterCommitIfTransaction } from '../../../../helpers/database-utils'
import { afterCommitIfTransaction } from '../../../helpers/database-utils' import { logger } from '../../../../helpers/logger'
import { logger } from '../../../helpers/logger' import { ActorModel } from '../../../../models/actor/actor'
import { ActorModel } from '../../../models/actor/actor' import { ActorFollowModel } from '../../../../models/actor/actor-follow'
import { ActorFollowModel } from '../../../models/actor/actor-follow' import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../../types/models'
import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' import { JobQueue } from '../../../job-queue'
import { JobQueue } from '../../job-queue' import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAudience } from './audience-utils'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
byActor: MActorLight byActor: MActorLight
@ -23,16 +23,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isOwned() === false) {
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor return sendVideoActivityToOrigin(activityBuilder, options)
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)
})
} }
// Send to followers // Send to followers
@ -44,6 +35,30 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType) 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 ( async function forwardVideoRelatedActivity (
activity: Activity, activity: Activity,
t: Transaction, t: Transaction,
@ -92,6 +107,8 @@ async function forwardActivity (
return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })) return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }))
} }
// ---------------------------------------------------------------------------
async function broadcastToFollowers ( async function broadcastToFollowers (
data: any, data: any,
byActor: MActorId, byActor: MActorId,
@ -177,6 +194,7 @@ export {
unicastTo, unicastTo,
forwardActivity, forwardActivity,
broadcastToActors, broadcastToActors,
sendVideoActivityToOrigin,
forwardVideoRelatedActivity, forwardVideoRelatedActivity,
sendVideoRelatedActivity sendVideoRelatedActivity
} }

View File

@ -87,7 +87,7 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
// Maybe it's a reply to a video? // Maybe it's a reply to a video?
// If yes, it's done: we resolved all the thread // 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 }) const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !video.hasPrivacyForFederation()) { if (video.isOwned() && !video.hasPrivacyForFederation()) {

View File

@ -1,36 +1,48 @@
import { map } from 'bluebird'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { doJSONRequest } from '@server/helpers/requests'
import { VideoRateType } from '../../../shared/models/videos' import { VideoRateType } from '../../../shared/models/videos'
import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { MAccountActor, MActorUrl, MVideoAccountLight, MVideoFullLight, MVideoId } from '../../types/models'
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 { sendLike, sendUndoDislike, sendUndoLike } from './send' import { sendLike, sendUndoDislike, sendUndoLike } from './send'
import { sendDislike } from './send/send-dislike' import { sendDislike } from './send/send-dislike'
import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
import { federateVideoIfNeeded } from './videos'
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 })
}
async function sendVideoRateChange ( 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, account: MAccountActor,
video: MVideoAccountLight, video: MVideoAccountLight,
likes: number, likes: number,
dislikes: number, dislikes: number,
t: Transaction t: Transaction
) { ) {
// Local video, we don't need to send like
if (video.isOwned()) return
const actor = account.Actor const actor = account.Actor
// Keep the order: first we undo and then we create // Keep the order: first we undo and then we create
@ -45,46 +57,3 @@ async function sendVideoRateChange (
// Dislike // Dislike
if (dislikes > 0) await sendDislike(actor, video, t) 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 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
// Default params // 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 fetchType = options.fetchType || 'all'
const allowRefresh = options.allowRefresh !== false 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 { logger, loggerTagsFactory } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { JobQueue } from '@server/lib/job-queue' 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 { VideoCommentModel } from '@server/models/video/video-comment'
import { VideoShareModel } from '@server/models/video/video-share' import { VideoShareModel } from '@server/models/video/video-share'
import { MVideo } from '@server/types/models' import { MVideo } from '@server/types/models'
import { ActivitypubHttpFetcherPayload, VideoObject } from '@shared/models' import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
import { crawlCollectionPage } from '../../crawl' import { crawlCollectionPage } from '../../crawl'
import { addVideoShares } from '../../share' import { addVideoShares } from '../../share'
import { addVideoComments } from '../../video-comments' import { addVideoComments } from '../../video-comments'
import { createRates } from '../../video-rates'
const lTags = loggerTagsFactory('ap', 'video') const lTags = loggerTagsFactory('ap', 'video')
type SyncParam = { type SyncParam = {
likes: boolean rates: boolean
dislikes: boolean
shares: boolean shares: boolean
comments: boolean comments: boolean
thumbnail: boolean thumbnail: boolean
@ -24,45 +24,57 @@ type SyncParam = {
async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) { async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) {
logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
await syncRates('like', video, fetchedVideo, syncParam.likes) const ratePromise = updateVideoRates(video, fetchedVideo)
await syncRates('dislike', video, fetchedVideo, syncParam.dislikes) if (syncParam.rates) await ratePromise
await syncShares(video, fetchedVideo, syncParam.shares) await syncShares(video, fetchedVideo, syncParam.shares)
await syncComments(video, fetchedVideo, syncParam.comments) 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 { export {
SyncParam, SyncParam,
syncVideoExternalAttributes syncVideoExternalAttributes,
updateVideoRates
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function createJob (payload: ActivitypubHttpFetcherPayload) { async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
}
function syncRates (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
const uri = type === 'like' const uri = type === 'like'
? fetchedVideo.likes ? fetchedVideo.likes
: fetchedVideo.dislikes : fetchedVideo.dislikes
if (!isSync) { logger.info('Sync %s of video %s', type, video.url)
const jobType = type === 'like' const options = { activityPub: true }
? 'video-likes'
: 'video-dislikes'
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) return totalItems
const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, type, crawlStartDate) }
return crawlCollectionPage<string>(uri, handler, cleaner) function createJob (payload: ActivitypubHttpFetcherPayload) {
.catch(err => logger.error('Cannot add rate of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) })) return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
} }
function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { 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 { VideoLiveModel } from '@server/models/video/video-live'
import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models' import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
import { VideoObject, VideoPrivacy } from '@shared/models' import { VideoObject, VideoPrivacy } from '@shared/models'
import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared' import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
export class APVideoUpdater extends APVideoAbstractBuilder { export class APVideoUpdater extends APVideoAbstractBuilder {
private readonly wasPrivateVideo: boolean private readonly wasPrivateVideo: boolean
@ -74,6 +74,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
transaction: undefined transaction: undefined
}) })
await updateVideoRates(videoUpdated, this.videoObject)
// Notify our users? // Notify our users?
if (this.wasPrivateVideo || this.wasUnlistedVideo) { if (this.wasPrivateVideo || this.wasUnlistedVideo) {
Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export {
async function refreshVideo (videoUrl: string) { async function refreshVideo (videoUrl: string) {
const fetchType = 'all' as 'all' 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) const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
if (videoFromDatabase) { if (videoFromDatabase) {

View File

@ -352,7 +352,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
// We need more attributes and check if the video still exists // We need more attributes and check if the video still exists
const getVideoOptions = { const getVideoOptions = {
videoObject: videoUrl, 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' fetchType: 'all' as 'all'
} }
const { video } = await getOrCreateAPVideo(getVideoOptions) 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 { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
import { ActorModel } from '../actor/actor' import { ActorModel } from '../actor/actor'
import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
import { AccountModel } from './account' import { AccountModel } from './account'
@ -249,28 +249,6 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
]).then(([ total, data ]) => ({ total, data })) ]).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 { toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
return { return {
video: this.Video.toFormattedJSON(), 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' const field = type === 'like'
? 'likes' ? 'likes'
: 'dislikes' : 'dislikes'

View File

@ -606,8 +606,8 @@ describe('Test multiple servers', function () {
for (const baseVideo of baseVideos) { for (const baseVideo of baseVideos) {
const sameVideo = data.find(video => video.name === baseVideo.name) const sameVideo = data.find(video => video.name === baseVideo.name)
expect(baseVideo.likes).to.equal(sameVideo.likes) expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
expect(baseVideo.dislikes).to.equal(sameVideo.dislikes) 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 './activity'
export * from './activitypub-actor' export * from './activitypub-actor'
export * from './activitypub-collection' export * from './activitypub-collection'
export * from './activitypub-ordered-collection' export * from './activitypub-ordered-collection'
export * from './activitypub-root' export * from './activitypub-root'
export * from './activitypub-signature' export * from './activitypub-signature'
export * from './objects' export * from './context'
export * from './webfinger' export * from './webfinger'

View File

@ -52,7 +52,7 @@ export type ActivitypubFollowPayload = {
assertIsChannel?: boolean 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 = { export type ActivitypubHttpFetcherPayload = {
uri: string uri: string
type: FetchType type: FetchType