1
0
Fork 0

Correctly forward like/dislikes and undo

This commit is contained in:
Chocobozzz 2017-11-24 13:41:10 +01:00
parent d4f1e94c89
commit 63c93323ec
No known key found for this signature in database
GPG key ID: 583A612D890159BE
15 changed files with 246 additions and 134 deletions

View file

@ -76,7 +76,7 @@ function addRemoteVideo (account: AccountInstance,
if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t) const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t)
if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.') if (videoFromDatabase) return videoFromDatabase
const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc) const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc)
const video = db.Video.build(videoData) const video = db.Video.build(videoData)

View file

@ -1,14 +1,14 @@
import { ActivityCreate, VideoChannelObject } from '../../../../shared' import { ActivityCreate, VideoChannelObject } from '../../../../shared'
import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object'
import { ViewObject } from '../../../../shared/models/activitypub/objects/view-object' import { ViewObject } from '../../../../shared/models/activitypub/objects/view-object'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { database as db } from '../../../initializers' import { database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface' import { AccountInstance } from '../../../models/account/account-interface'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateAccountAndServer } from '../account'
import { sendCreateDislikeToVideoFollowers, sendCreateViewToVideoFollowers } from '../send/send-create' import { forwardActivity } from '../send/misc'
import { getVideoChannelActivityPubUrl } from '../url' import { getVideoChannelActivityPubUrl } from '../url'
import { videoChannelActivityObjectToDBAttributes } from './misc' import { videoChannelActivityObjectToDBAttributes } from './misc'
import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
async function processCreateActivity (activity: ActivityCreate) { async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object const activityObject = activity.object
@ -16,9 +16,9 @@ async function processCreateActivity (activity: ActivityCreate) {
const account = await getOrCreateAccountAndServer(activity.actor) const account = await getOrCreateAccountAndServer(activity.actor)
if (activityType === 'View') { if (activityType === 'View') {
return processCreateView(activityObject as ViewObject) return processCreateView(account, activity)
} else if (activityType === 'Dislike') { } else if (activityType === 'Dislike') {
return processCreateDislike(account, activityObject as DislikeObject) return processCreateDislike(account, activity)
} else if (activityType === 'VideoChannel') { } else if (activityType === 'VideoChannel') {
return processCreateVideoChannel(account, activityObject as VideoChannelObject) return processCreateVideoChannel(account, activityObject as VideoChannelObject)
} else if (activityType === 'Flag') { } else if (activityType === 'Flag') {
@ -37,19 +37,20 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processCreateDislike (byAccount: AccountInstance, dislike: DislikeObject) { async function processCreateDislike (byAccount: AccountInstance, activity: ActivityCreate) {
const options = { const options = {
arguments: [ byAccount, dislike ], arguments: [ byAccount, activity ],
errorMessage: 'Cannot dislike the video with many retries.' errorMessage: 'Cannot dislike the video with many retries.'
} }
return retryTransactionWrapper(createVideoDislike, options) return retryTransactionWrapper(createVideoDislike, options)
} }
function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject) { function createVideoDislike (byAccount: AccountInstance, activity: ActivityCreate) {
return db.sequelize.transaction(async t => { const dislike = activity.object as DislikeObject
const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object)
return db.sequelize.transaction(async t => {
const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object, t)
if (!video) throw new Error('Unknown video ' + dislike.object) if (!video) throw new Error('Unknown video ' + dislike.object)
const rate = { const rate = {
@ -59,15 +60,22 @@ function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject)
} }
const [ , created ] = await db.AccountVideoRate.findOrCreate({ const [ , created ] = await db.AccountVideoRate.findOrCreate({
where: rate, where: rate,
defaults: rate defaults: rate,
transaction: t
}) })
await video.increment('dislikes') await video.increment('dislikes', { transaction: t })
if (video.isOwned() && created === true) await sendCreateDislikeToVideoFollowers(byAccount, video, undefined) if (video.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
await forwardActivity(activity, t, exceptions)
}
}) })
} }
async function processCreateView (view: ViewObject) { async function processCreateView (byAccount: AccountInstance, activity: ActivityCreate) {
const view = activity.object as ViewObject
const video = await db.Video.loadByUrlAndPopulateAccount(view.object) const video = await db.Video.loadByUrlAndPopulateAccount(view.object)
if (!video) throw new Error('Unknown video ' + view.object) if (!video) throw new Error('Unknown video ' + view.object)
@ -77,7 +85,11 @@ async function processCreateView (view: ViewObject) {
await video.increment('views') await video.increment('views')
if (video.isOwned()) await sendCreateViewToVideoFollowers(account, video, undefined) if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
await forwardActivity(activity, undefined, exceptions)
}
} }
function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
@ -94,7 +106,7 @@ function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateDa
return db.sequelize.transaction(async t => { return db.sequelize.transaction(async t => {
let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') if (videoChannel) return videoChannel
const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
videoChannel = db.VideoChannel.build(videoChannelData) videoChannel = db.VideoChannel.build(videoChannelData)

View file

@ -1,14 +1,14 @@
import { ActivityLike } from '../../../../shared/models/activitypub/activity' import { ActivityLike } from '../../../../shared/models/activitypub/activity'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { database as db } from '../../../initializers' import { database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface' import { AccountInstance } from '../../../models/account/account-interface'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateAccountAndServer } from '../account'
import { sendLikeToVideoFollowers } from '../send/send-like' import { forwardActivity } from '../send/misc'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
async function processLikeActivity (activity: ActivityLike) { async function processLikeActivity (activity: ActivityLike) {
const account = await getOrCreateAccountAndServer(activity.actor) const account = await getOrCreateAccountAndServer(activity.actor)
return processLikeVideo(account, activity.object) return processLikeVideo(account, activity)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -19,16 +19,18 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processLikeVideo (byAccount: AccountInstance, videoUrl: string) { async function processLikeVideo (byAccount: AccountInstance, activity: ActivityLike) {
const options = { const options = {
arguments: [ byAccount, videoUrl ], arguments: [ byAccount, activity ],
errorMessage: 'Cannot like the video with many retries.' errorMessage: 'Cannot like the video with many retries.'
} }
return retryTransactionWrapper(createVideoLike, options) return retryTransactionWrapper(createVideoLike, options)
} }
function createVideoLike (byAccount: AccountInstance, videoUrl: string) { function createVideoLike (byAccount: AccountInstance, activity: ActivityLike) {
const videoUrl = activity.object
return db.sequelize.transaction(async t => { return db.sequelize.transaction(async t => {
const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl) const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl)
@ -41,10 +43,15 @@ function createVideoLike (byAccount: AccountInstance, videoUrl: string) {
} }
const [ , created ] = await db.AccountVideoRate.findOrCreate({ const [ , created ] = await db.AccountVideoRate.findOrCreate({
where: rate, where: rate,
defaults: rate defaults: rate,
transaction: t
}) })
await video.increment('likes') await video.increment('likes', { transaction: t })
if (video.isOwned() && created === true) await sendLikeToVideoFollowers(byAccount, video, undefined) if (video.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
await forwardActivity(activity, t, exceptions)
}
}) })
} }

View file

@ -3,16 +3,15 @@ import { DislikeObject } from '../../../../shared/models/activitypub/objects/dis
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { database as db } from '../../../initializers' import { database as db } from '../../../initializers'
import { sendUndoDislikeToVideoFollowers } from '../index' import { forwardActivity } from '../send/misc'
import { sendUndoLikeToVideoFollowers } from '../send/send-undo'
async function processUndoActivity (activity: ActivityUndo) { async function processUndoActivity (activity: ActivityUndo) {
const activityToUndo = activity.object const activityToUndo = activity.object
if (activityToUndo.type === 'Like') { if (activityToUndo.type === 'Like') {
return processUndoLike(activity.actor, activityToUndo) return processUndoLike(activity.actor, activity)
} else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') { } else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') {
return processUndoDislike(activity.actor, activityToUndo.object) return processUndoDislike(activity.actor, activity)
} else if (activityToUndo.type === 'Follow') { } else if (activityToUndo.type === 'Follow') {
return processUndoFollow(activity.actor, activityToUndo) return processUndoFollow(activity.actor, activityToUndo)
} }
@ -30,57 +29,69 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function processUndoLike (actor: string, likeActivity: ActivityLike) { function processUndoLike (actor: string, activity: ActivityUndo) {
const options = { const options = {
arguments: [ actor, likeActivity ], arguments: [ actor, activity ],
errorMessage: 'Cannot undo like with many retries.' errorMessage: 'Cannot undo like with many retries.'
} }
return retryTransactionWrapper(undoLike, options) return retryTransactionWrapper(undoLike, options)
} }
function undoLike (actor: string, likeActivity: ActivityLike) { function undoLike (actor: string, activity: ActivityUndo) {
const likeActivity = activity.object as ActivityLike
return db.sequelize.transaction(async t => { return db.sequelize.transaction(async t => {
const byAccount = await db.Account.loadByUrl(actor, t) const byAccount = await db.Account.loadByUrl(actor, t)
if (!byAccount) throw new Error('Unknown account ' + actor) if (!byAccount) throw new Error('Unknown account ' + actor)
const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object) const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object, t)
if (!video) throw new Error('Unknown video ' + likeActivity.actor) if (!video) throw new Error('Unknown video ' + likeActivity.actor)
const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t) const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })
await video.decrement('likes') await video.decrement('likes', { transaction: t })
if (video.isOwned()) await sendUndoLikeToVideoFollowers(byAccount, video, t) if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
await forwardActivity(activity, t, exceptions)
}
}) })
} }
function processUndoDislike (actor: string, dislikeCreateActivity: DislikeObject) { function processUndoDislike (actor: string, activity: ActivityUndo) {
const options = { const options = {
arguments: [ actor, dislikeCreateActivity ], arguments: [ actor, activity ],
errorMessage: 'Cannot undo dislike with many retries.' errorMessage: 'Cannot undo dislike with many retries.'
} }
return retryTransactionWrapper(undoDislike, options) return retryTransactionWrapper(undoDislike, options)
} }
function undoDislike (actor: string, dislike: DislikeObject) { function undoDislike (actor: string, activity: ActivityUndo) {
const dislike = activity.object.object as DislikeObject
return db.sequelize.transaction(async t => { return db.sequelize.transaction(async t => {
const byAccount = await db.Account.loadByUrl(actor, t) const byAccount = await db.Account.loadByUrl(actor, t)
if (!byAccount) throw new Error('Unknown account ' + actor) if (!byAccount) throw new Error('Unknown account ' + actor)
const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object) const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object, t)
if (!video) throw new Error('Unknown video ' + dislike.actor) if (!video) throw new Error('Unknown video ' + dislike.actor)
const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t) const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })
await video.decrement('dislikes') await video.decrement('dislikes', { transaction: t })
if (video.isOwned()) await sendUndoDislikeToVideoFollowers(byAccount, video, t) if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
await forwardActivity(activity, t, exceptions)
}
}) })
} }

View file

@ -2,8 +2,45 @@ import { Transaction } from 'sequelize'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { ACTIVITY_PUB, database as db } from '../../../initializers' import { ACTIVITY_PUB, database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface' import { AccountInstance } from '../../../models/account/account-interface'
import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' import {
activitypubHttpJobScheduler,
ActivityPubHttpPayload
} from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler'
import { VideoInstance } from '../../../models/video/video-interface' import { VideoInstance } from '../../../models/video/video-interface'
import { Activity } from '../../../../shared/models/activitypub/activity'
async function forwardActivity (
activity: Activity,
t: Transaction,
followersException: AccountInstance[] = []
) {
const to = activity.to || []
const cc = activity.cc || []
const followersUrls: string[] = []
for (const dest of to.concat(cc)) {
if (dest.endsWith('/followers')) {
followersUrls.push(dest)
}
}
const toAccountFollowers = await db.Account.listByFollowersUrls(followersUrls)
const uris = await computeFollowerUris(toAccountFollowers, followersException)
if (uris.length === 0) {
logger.info('0 followers for %s, no forwarding.', toAccountFollowers.map(a => a.id).join(', '))
return
}
logger.debug('Creating forwarding job.', { uris })
const jobPayload: ActivityPubHttpPayload = {
uris,
body: activity
}
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload)
}
async function broadcastToFollowers ( async function broadcastToFollowers (
data: any, data: any,
@ -12,18 +49,15 @@ async function broadcastToFollowers (
t: Transaction, t: Transaction,
followersException: AccountInstance[] = [] followersException: AccountInstance[] = []
) { ) {
const toAccountFollowerIds = toAccountFollowers.map(a => a.id) const uris = await computeFollowerUris(toAccountFollowers, followersException)
if (uris.length === 0) {
const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) logger.info('0 followers for %s, no broadcasting.', toAccountFollowers.map(a => a.id).join(', '))
if (result.data.length === 0) { return
logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', '))
return undefined
} }
const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) logger.debug('Creating broadcast job.', { uris })
const uris = result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1)
const jobPayload = { const jobPayload: ActivityPubHttpPayload = {
uris, uris,
signatureAccountId: byAccount.id, signatureAccountId: byAccount.id,
body: data body: data
@ -33,7 +67,9 @@ async function broadcastToFollowers (
} }
async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) {
const jobPayload = { logger.debug('Creating unicast job.', { uri: toAccountUrl })
const jobPayload: ActivityPubHttpPayload = {
uris: [ toAccountUrl ], uris: [ toAccountUrl ],
signatureAccountId: byAccount.id, signatureAccountId: byAccount.id,
body: data body: data
@ -42,21 +78,21 @@ async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: s
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
} }
function getOriginVideoAudience (video: VideoInstance) { function getOriginVideoAudience (video: VideoInstance, accountsInvolvedInVideo: AccountInstance[]) {
return { return {
to: [ video.VideoChannel.Account.url ], to: [ video.VideoChannel.Account.url ],
cc: [ video.VideoChannel.Account.url + '/followers' ] cc: accountsInvolvedInVideo.map(a => a.followersUrl)
} }
} }
function getVideoFollowersAudience (video: VideoInstance) { function getVideoFollowersAudience (accountsInvolvedInVideo: AccountInstance[]) {
return { return {
to: [ video.VideoChannel.Account.url + '/followers' ], to: accountsInvolvedInVideo.map(a => a.followersUrl),
cc: [] cc: []
} }
} }
async function getAccountsToForwardVideoAction (byAccount: AccountInstance, video: VideoInstance) { async function getAccountsInvolvedInVideo (video: VideoInstance) {
const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id) const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
accountsToForwardView.push(video.VideoChannel.Account) accountsToForwardView.push(video.VideoChannel.Account)
@ -81,6 +117,16 @@ async function getAudience (accountSender: AccountInstance, isPublic = true) {
return { to, cc } return { to, cc }
} }
async function computeFollowerUris (toAccountFollower: AccountInstance[], followersException: AccountInstance[]) {
const toAccountFollowerIds = toAccountFollower.map(a => a.id)
const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds)
const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl)
const uris = result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1)
return uris
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -88,6 +134,7 @@ export {
unicastTo, unicastTo,
getAudience, getAudience,
getOriginVideoAudience, getOriginVideoAudience,
getAccountsToForwardVideoAction, getAccountsInvolvedInVideo,
getVideoFollowersAudience getVideoFollowersAudience,
forwardActivity
} }

View file

@ -1,12 +1,12 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityCreate } from '../../../../shared/models/activitypub/activity' import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub/activity'
import { getServerAccount } from '../../../helpers/utils' import { getServerAccount } from '../../../helpers/utils'
import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsToForwardVideoAction, getAccountsInvolvedInVideo,
getAudience, getAudience,
getOriginVideoAudience, getOriginVideoAudience,
getVideoFollowersAudience, getVideoFollowersAudience,
@ -35,7 +35,8 @@ async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoI
const url = getVideoViewActivityPubUrl(byAccount, video) const url = getVideoViewActivityPubUrl(byAccount, video)
const viewActivity = createViewActivityData(byAccount, video) const viewActivity = createViewActivityData(byAccount, video)
const audience = getOriginVideoAudience(video) const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
const data = await createActivityData(url, byAccount, viewActivity, audience) const data = await createActivityData(url, byAccount, viewActivity, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@ -45,12 +46,12 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video
const url = getVideoViewActivityPubUrl(byAccount, video) const url = getVideoViewActivityPubUrl(byAccount, video)
const viewActivity = createViewActivityData(byAccount, video) const viewActivity = createViewActivityData(byAccount, video)
const audience = getVideoFollowersAudience(video) const accountsToForwardView = await getAccountsInvolvedInVideo(video)
const audience = getVideoFollowersAudience(accountsToForwardView)
const data = await createActivityData(url, byAccount, viewActivity, audience) const data = await createActivityData(url, byAccount, viewActivity, audience)
// Use the server account to send the view, because it could be an unregistered account
const serverAccount = await getServerAccount() const serverAccount = await getServerAccount()
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const followersException = [ byAccount ] const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
} }
@ -59,7 +60,8 @@ async function sendCreateDislikeToOrigin (byAccount: AccountInstance, video: Vid
const url = getVideoDislikeActivityPubUrl(byAccount, video) const url = getVideoDislikeActivityPubUrl(byAccount, video)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byAccount, video)
const audience = getOriginVideoAudience(video) const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
const data = await createActivityData(url, byAccount, dislikeActivity, audience) const data = await createActivityData(url, byAccount, dislikeActivity, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@ -69,17 +71,15 @@ async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, vi
const url = getVideoDislikeActivityPubUrl(byAccount, video) const url = getVideoDislikeActivityPubUrl(byAccount, video)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byAccount, video)
const audience = getVideoFollowersAudience(video) const accountsToForwardView = await getAccountsInvolvedInVideo(video)
const audience = getVideoFollowersAudience(accountsToForwardView)
const data = await createActivityData(url, byAccount, dislikeActivity, audience) const data = await createActivityData(url, byAccount, dislikeActivity, audience)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ] const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, byAccount, accountsToForwardView, t, followersException)
} }
async function createActivityData (url: string, byAccount: AccountInstance, object: any, audience?: { to: string[], cc: string[] }) { async function createActivityData (url: string, byAccount: AccountInstance, object: any, audience?: ActivityAudience) {
if (!audience) { if (!audience) {
audience = await getAudience(byAccount) audience = await getAudience(byAccount)
} }

View file

@ -1,11 +1,10 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityLike } from '../../../../shared/models/activitypub/activity' import { ActivityLike } from '../../../../shared/models/activitypub/activity'
import { getServerAccount } from '../../../helpers/utils'
import { AccountInstance, VideoInstance } from '../../../models' import { AccountInstance, VideoInstance } from '../../../models'
import { getVideoLikeActivityPubUrl } from '../url' import { getVideoLikeActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsToForwardVideoAction, getAccountsInvolvedInVideo,
getAudience, getAudience,
getOriginVideoAudience, getOriginVideoAudience,
getVideoFollowersAudience, getVideoFollowersAudience,
@ -15,7 +14,8 @@ import {
async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video) const url = getVideoLikeActivityPubUrl(byAccount, video)
const audience = getOriginVideoAudience(video) const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
const data = await likeActivityData(url, byAccount, video, audience) const data = await likeActivityData(url, byAccount, video, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@ -24,14 +24,14 @@ async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstanc
async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video) const url = getVideoLikeActivityPubUrl(byAccount, video)
const audience = getVideoFollowersAudience(video) const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
const audience = getVideoFollowersAudience(accountsInvolvedInVideo)
const data = await likeActivityData(url, byAccount, video, audience) const data = await likeActivityData(url, byAccount, video, audience)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) const toAccountsFollowers = await getAccountsInvolvedInVideo(video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ] const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException)
} }
async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) { async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) {

View file

@ -1,11 +1,16 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity' import {
import { getServerAccount } from '../../../helpers/utils' ActivityAudience,
ActivityCreate,
ActivityFollow,
ActivityLike,
ActivityUndo
} from '../../../../shared/models/activitypub/activity'
import { AccountInstance } from '../../../models' import { AccountInstance } from '../../../models'
import { AccountFollowInstance } from '../../../models/account/account-follow-interface' import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
import { VideoInstance } from '../../../models/video/video-interface' import { VideoInstance } from '../../../models/video/video-interface'
import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
import { broadcastToFollowers, getAccountsToForwardVideoAction, unicastTo } from './misc' import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getVideoFollowersAudience, unicastTo } from './misc'
import { createActivityData, createDislikeActivityData } from './send-create' import { createActivityData, createDislikeActivityData } from './send-create'
import { followActivityData } from './send-follow' import { followActivityData } from './send-follow'
import { likeActivityData } from './send-like' import { likeActivityData } from './send-like'
@ -37,14 +42,13 @@ async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video:
const likeUrl = getVideoLikeActivityPubUrl(byAccount, video) const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
const undoUrl = getUndoActivityPubUrl(likeUrl) const undoUrl = getUndoActivityPubUrl(likeUrl)
const toAccountsFollowers = await getAccountsInvolvedInVideo(video)
const audience = getVideoFollowersAudience(toAccountsFollowers)
const object = await likeActivityData(likeUrl, byAccount, video) const object = await likeActivityData(likeUrl, byAccount, video)
const data = await undoActivityData(undoUrl, byAccount, object) const data = await undoActivityData(undoUrl, byAccount, object, audience)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ] const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException)
} }
async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
@ -68,11 +72,10 @@ async function sendUndoDislikeToVideoFollowers (byAccount: AccountInstance, vide
const data = await undoActivityData(undoUrl, byAccount, object) const data = await undoActivityData(undoUrl, byAccount, object)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) const toAccountsFollowers = await getAccountsInvolvedInVideo(video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ] const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -87,11 +90,22 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow | ActivityLike | ActivityCreate) { async function undoActivityData (
url: string,
byAccount: AccountInstance,
object: ActivityFollow | ActivityLike | ActivityCreate,
audience?: ActivityAudience
) {
if (!audience) {
audience = await getAudience(byAccount)
}
const activity: ActivityUndo = { const activity: ActivityUndo = {
type: 'Undo', type: 'Undo',
id: url, id: url,
actor: byAccount.url, actor: byAccount.url,
to: audience.to,
cc: audience.cc,
object object
} }

View file

@ -1,21 +1,16 @@
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { buildSignedActivity } from '../../../helpers/activitypub'
import { doRequest } from '../../../helpers/requests' import { doRequest } from '../../../helpers/requests'
import { database as db } from '../../../initializers' import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
async function process (payload: ActivityPubHttpPayload, jobId: number) { async function process (payload: ActivityPubHttpPayload, jobId: number) {
logger.info('Processing ActivityPub broadcast in job %d.', jobId) logger.info('Processing ActivityPub broadcast in job %d.', jobId)
const accountSignature = await db.Account.load(payload.signatureAccountId) const body = await computeBody(payload)
if (!accountSignature) throw new Error('Unknown signature account id.')
const signedBody = await buildSignedActivity(accountSignature, payload.body)
const options = { const options = {
method: 'POST', method: 'POST',
uri: '', uri: '',
json: signedBody json: body
} }
for (const uri of payload.uris) { for (const uri of payload.uris) {

View file

@ -1,11 +1,13 @@
import { JobScheduler, JobHandler } from '../job-scheduler' import { JobCategory } from '../../../../shared'
import { buildSignedActivity } from '../../../helpers/activitypub'
import { logger } from '../../../helpers/logger'
import { ACTIVITY_PUB } from '../../../initializers/constants'
import { database as db } from '../../../initializers/database'
import { JobHandler, JobScheduler } from '../job-scheduler'
import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler' import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler'
import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler'
import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler' import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler'
import { JobCategory } from '../../../../shared' import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler'
import { ACTIVITY_PUB } from '../../../initializers/constants'
import { logger } from '../../../helpers/logger'
type ActivityPubHttpPayload = { type ActivityPubHttpPayload = {
uris: string[] uris: string[]
@ -40,8 +42,21 @@ function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, ur
} }
} }
async function computeBody (payload: ActivityPubHttpPayload) {
let body = payload.body
if (payload.signatureAccountId) {
const accountSignature = await db.Account.load(payload.signatureAccountId)
if (!accountSignature) throw new Error('Unknown signature account id.')
body = await buildSignedActivity(accountSignature, payload.body)
}
return body
}
export { export {
ActivityPubHttpPayload, ActivityPubHttpPayload,
activitypubHttpJobScheduler, activitypubHttpJobScheduler,
maybeRetryRequestLater maybeRetryRequestLater,
computeBody
} }

View file

@ -1,21 +1,17 @@
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { doRequest } from '../../../helpers/requests' import { doRequest } from '../../../helpers/requests'
import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler' import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
import { database as db } from '../../../initializers/database'
import { buildSignedActivity } from '../../../helpers/activitypub'
async function process (payload: ActivityPubHttpPayload, jobId: number) { async function process (payload: ActivityPubHttpPayload, jobId: number) {
logger.info('Processing ActivityPub unicast in job %d.', jobId) logger.info('Processing ActivityPub unicast in job %d.', jobId)
const accountSignature = await db.Account.load(payload.signatureAccountId) const body = await computeBody(payload)
if (!accountSignature) throw new Error('Unknown signature account id.')
const signedBody = await buildSignedActivity(accountSignature, payload.body)
const uri = payload.uris[0] const uri = payload.uris[0]
const options = { const options = {
method: 'POST', method: 'POST',
uri, uri,
json: signedBody json: body
} }
try { try {

View file

@ -12,6 +12,7 @@ export namespace AccountMethods {
export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance> export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadLocalByName = (name: string) => Bluebird<AccountInstance> export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance> export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
export type ListByFollowersUrls = (followerUrls: string[], transaction?: Sequelize.Transaction) => Bluebird<AccountInstance[]>
export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
@ -29,6 +30,7 @@ export interface AccountClass {
loadByUrl: AccountMethods.LoadByUrl loadByUrl: AccountMethods.LoadByUrl
loadLocalByName: AccountMethods.LoadLocalByName loadLocalByName: AccountMethods.LoadLocalByName
loadByNameAndHost: AccountMethods.LoadByNameAndHost loadByNameAndHost: AccountMethods.LoadByNameAndHost
listByFollowersUrls: AccountMethods.ListByFollowersUrls
} }
export interface AccountAttributes { export interface AccountAttributes {

View file

@ -26,6 +26,7 @@ let loadByUUID: AccountMethods.LoadByUUID
let loadByUrl: AccountMethods.LoadByUrl let loadByUrl: AccountMethods.LoadByUrl
let loadLocalByName: AccountMethods.LoadLocalByName let loadLocalByName: AccountMethods.LoadLocalByName
let loadByNameAndHost: AccountMethods.LoadByNameAndHost let loadByNameAndHost: AccountMethods.LoadByNameAndHost
let listByFollowersUrls: AccountMethods.ListByFollowersUrls
let isOwned: AccountMethods.IsOwned let isOwned: AccountMethods.IsOwned
let toActivityPubObject: AccountMethods.ToActivityPubObject let toActivityPubObject: AccountMethods.ToActivityPubObject
let toFormattedJSON: AccountMethods.ToFormattedJSON let toFormattedJSON: AccountMethods.ToFormattedJSON
@ -188,7 +189,8 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
loadByUUID, loadByUUID,
loadByUrl, loadByUrl,
loadLocalByName, loadLocalByName,
loadByNameAndHost loadByNameAndHost,
listByFollowersUrls
] ]
const instanceMethods = [ const instanceMethods = [
isOwned, isOwned,
@ -427,3 +429,16 @@ loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
return Account.findOne(query) return Account.findOne(query)
} }
listByFollowersUrls = function (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
followersUrl: {
[Sequelize.Op.in]: followersUrls
}
},
transaction
}
return Account.findAll(query)
}

View file

@ -6,27 +6,8 @@ import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { VideoPrivacy, VideoResolution } from '../../../shared' import { VideoPrivacy, VideoResolution } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
import {
createTorrentPromise,
generateImageFromVideoFile,
getVideoFileHeight,
isVideoCategoryValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoLanguageValid,
isVideoLicenceValid,
isVideoNameValid,
isVideoNSFWValid,
isVideoPrivacyValid,
logger,
renamePromise,
statPromise,
transcode,
unlinkPromise,
writeFilePromise
} from '../../helpers'
import { activityPubCollection } from '../../helpers/activitypub' import { activityPubCollection } from '../../helpers/activitypub'
import { isVideoUrlValid } from '../../helpers/custom-validators/videos' import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid, isVideoUrlValid } from '../../helpers/custom-validators/videos'
import { import {
API_VERSION, API_VERSION,
CONFIG, CONFIG,
@ -39,7 +20,7 @@ import {
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_PRIVACIES VIDEO_PRIVACIES
} from '../../initializers' } from '../../initializers/constants'
import { sendDeleteVideo } from '../../lib/index' import { sendDeleteVideo } from '../../lib/index'
import { addMethodsToModel, getSort } from '../utils' import { addMethodsToModel, getSort } from '../utils'
@ -47,6 +28,10 @@ import { addMethodsToModel, getSort } from '../utils'
import { TagInstance } from './tag-interface' import { TagInstance } from './tag-interface'
import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { VideoFileInstance, VideoFileModel } from './video-file-interface'
import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
import { isVideoNameValid, isVideoLicenceValid, isVideoNSFWValid, isVideoDescriptionValid, isVideoDurationValid } from '../../helpers/index'
import { logger } from '../../helpers/logger'
import { generateImageFromVideoFile, transcode, getVideoFileHeight } from '../../helpers/ffmpeg-utils'
import { createTorrentPromise, writeFilePromise, unlinkPromise, renamePromise, statPromise } from '../../helpers/core-utils'
let Video: Sequelize.Model<VideoInstance, VideoAttributes> let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile let getOriginalFile: VideoMethods.GetOriginalFile
@ -1013,6 +998,10 @@ loadAndPopulateAccountAndServerAndTags = function (id: number) {
model: Video['sequelize'].models.AccountVideoRate, model: Video['sequelize'].models.AccountVideoRate,
include: [ Video['sequelize'].models.Account ] include: [ Video['sequelize'].models.Account ]
}, },
{
model: Video['sequelize'].models.VideoShare,
include: [ Video['sequelize'].models.Account ]
},
Video['sequelize'].models.Tag, Video['sequelize'].models.Tag,
Video['sequelize'].models.VideoFile Video['sequelize'].models.VideoFile
] ]
@ -1040,6 +1029,10 @@ loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) {
model: Video['sequelize'].models.AccountVideoRate, model: Video['sequelize'].models.AccountVideoRate,
include: [ Video['sequelize'].models.Account ] include: [ Video['sequelize'].models.Account ]
}, },
{
model: Video['sequelize'].models.VideoShare,
include: [ Video['sequelize'].models.Account ]
},
Video['sequelize'].models.Tag, Video['sequelize'].models.Tag,
Video['sequelize'].models.VideoFile Video['sequelize'].models.VideoFile
] ]

View file

@ -10,6 +10,11 @@ export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like'
export interface ActivityAudience {
to: string[]
cc: string[]
}
export interface BaseActivity { export interface BaseActivity {
'@context'?: any[] '@context'?: any[]
id: string id: string