From 40ff57078e15d5b86ee6b71e198b95d3feb78eaf Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 22 Nov 2017 16:25:03 +0100 Subject: Federate video views --- server/lib/activitypub/process/misc.ts | 7 ++- server/lib/activitypub/process/process-create.ts | 19 ++++++- server/lib/activitypub/process/process-follow.ts | 6 ++ server/lib/activitypub/send/misc.ts | 14 ++++- server/lib/activitypub/send/send-create.ts | 64 +++++++++++++++++++--- server/lib/activitypub/url.ts | 7 ++- .../activitypub-http-fetcher-handler.ts | 6 +- 7 files changed, 106 insertions(+), 17 deletions(-) (limited to 'server/lib') diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts index e90a793fc..eefbe2884 100644 --- a/server/lib/activitypub/process/misc.ts +++ b/server/lib/activitypub/process/misc.ts @@ -33,13 +33,18 @@ async function videoActivityObjectToDBAttributes ( else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED const duration = videoObject.duration.replace(/[^\d]+/, '') + let language = null + if (videoObject.language) { + language = parseInt(videoObject.language.identifier, 10) + } + const videoData: VideoAttributes = { name: videoObject.name, uuid: videoObject.uuid, url: videoObject.id, category: parseInt(videoObject.category.identifier, 10), licence: parseInt(videoObject.licence.identifier, 10), - language: parseInt(videoObject.language.identifier, 10), + language, nsfw: videoObject.nsfw, description: videoObject.content, channelId: videoChannel.id, diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index ddf7c74f6..1777733a0 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,9 +1,11 @@ import { ActivityCreate, VideoChannelObject } from '../../../../shared' import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' +import { ViewObject } from '../../../../shared/models/activitypub/objects/view-object' import { logger, retryTransactionWrapper } from '../../../helpers' import { database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' import { getOrCreateAccountAndServer } from '../account' +import { sendCreateViewToVideoFollowers } from '../send/send-create' import { getVideoChannelActivityPubUrl } from '../url' import { videoChannelActivityObjectToDBAttributes } from './misc' @@ -12,7 +14,9 @@ async function processCreateActivity (activity: ActivityCreate) { const activityType = activityObject.type const account = await getOrCreateAccountAndServer(activity.actor) - if (activityType === 'VideoChannel') { + if (activityType === 'View') { + return processCreateView(activityObject as ViewObject) + } else if (activityType === 'VideoChannel') { return processCreateVideoChannel(account, activityObject as VideoChannelObject) } else if (activityType === 'Flag') { return processCreateVideoAbuse(account, activityObject as VideoAbuseObject) @@ -30,6 +34,19 @@ export { // --------------------------------------------------------------------------- +async function processCreateView (view: ViewObject) { + const video = await db.Video.loadByUrlAndPopulateAccount(view.object) + + if (!video) throw new Error('Unknown video ' + view.object) + + const account = await db.Account.loadByUrl(view.actor) + if (!account) throw new Error('Unknown account ' + view.actor) + + await video.increment('views') + + if (video.isOwned()) await sendCreateViewToVideoFollowers(account, video, undefined) +} + function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { const options = { arguments: [ account, videoChannelToCreateData ], diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 248004226..320dc1138 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -49,6 +49,12 @@ async function follow (account: AccountInstance, targetAccountURL: string) { }, transaction: t }) + + if (accountFollow.state !== 'accepted') { + accountFollow.state = 'accepted' + await accountFollow.save({ transaction: t }) + } + accountFollow.AccountFollower = account accountFollow.AccountFollowing = targetAccount diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index bea955b67..f3dc5c148 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts @@ -4,16 +4,26 @@ import { ACTIVITY_PUB, database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' -async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) { +async function broadcastToFollowers ( + data: any, + byAccount: AccountInstance, + toAccountFollowers: AccountInstance[], + t: Transaction, + followersException: AccountInstance[] = [] +) { const toAccountFollowerIds = toAccountFollowers.map(a => a.id) + const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) if (result.data.length === 0) { logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) return undefined } + const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) + const uris = result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1) + const jobPayload = { - uris: result.data, + uris, signatureAccountId: byAccount.id, body: data } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index df8e0a642..e5fb212b7 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -3,7 +3,9 @@ import { ActivityCreate } from '../../../../shared/models/activitypub/activity' import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' import { broadcastToFollowers, getAudience, unicastTo } from './misc' -import { getVideoAbuseActivityPubUrl } from '../url' +import { getVideoAbuseActivityPubUrl, getVideoViewActivityPubUrl } from '../url' +import { getServerAccount } from '../../../helpers/utils' +import { database as db } from '../../../initializers' async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { const byAccount = videoChannel.Account @@ -16,21 +18,53 @@ async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Tr async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) { const url = getVideoAbuseActivityPubUrl(videoAbuse) - const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject()) + + const audience = { to: [ video.VideoChannel.Account.url ], cc: [] } + const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject(), audience) + + return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) +} + +async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const url = getVideoViewActivityPubUrl(byAccount, video) + const viewActivity = createViewActivityData(byAccount, video) + + const audience = { to: [ video.VideoChannel.Account.url ], cc: [ video.VideoChannel.Account.url + '/followers' ] } + const data = await createActivityData(url, byAccount, viewActivity, audience) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) } -// async function sendCreateView () +async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const url = getVideoViewActivityPubUrl(byAccount, video) + const viewActivity = createViewActivityData(byAccount, video) + + const audience = { to: [ video.VideoChannel.Account.url + '/followers' ], cc: [] } + const data = await createActivityData(url, byAccount, viewActivity, audience) + + const serverAccount = await getServerAccount() + const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id) + accountsToForwardView.push(video.VideoChannel.Account) + + // Don't forward view to server that sent it to us + const index = accountsToForwardView.findIndex(a => a.id === byAccount.id) + if (index) accountsToForwardView.splice(index, 1) + + const followersException = [ byAccount ] + return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) +} + +async function createActivityData (url: string, byAccount: AccountInstance, object: any, audience?: { to: string[], cc: string[] }) { + if (!audience) { + audience = await getAudience(byAccount) + } -async function createActivityData (url: string, byAccount: AccountInstance, object: any) { - const { to, cc } = await getAudience(byAccount) const activity: ActivityCreate = { type: 'Create', id: url, actor: byAccount.url, - to, - cc, + to: audience.to, + cc: audience.cc, object } @@ -42,5 +76,19 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje export { sendCreateVideoChannel, sendVideoAbuse, - createActivityData + createActivityData, + sendCreateViewToOrigin, + sendCreateViewToVideoFollowers +} + +// --------------------------------------------------------------------------- + +function createViewActivityData (byAccount: AccountInstance, video: VideoInstance) { + const obj = { + type: 'View', + actor: byAccount.url, + object: video.url + } + + return obj } diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 41ac0f9a8..d98561e33 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -21,6 +21,10 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) { return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id } +function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) { + return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString() +} + function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) { const me = accountFollow.AccountFollower const following = accountFollow.AccountFollowing @@ -56,5 +60,6 @@ export { getAccountFollowAcceptActivityPubUrl, getAnnounceActivityPubUrl, getUpdateActivityPubUrl, - getUndoActivityPubUrl + getUndoActivityPubUrl, + getVideoViewActivityPubUrl } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-fetcher-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-fetcher-handler.ts index 09efaa622..bda319592 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-fetcher-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-fetcher-handler.ts @@ -1,10 +1,8 @@ import { logger } from '../../../helpers' -import { buildSignedActivity } from '../../../helpers/activitypub' import { doRequest } from '../../../helpers/requests' -import { database as db } from '../../../initializers' -import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler' -import { processActivities } from '../../activitypub/process/process' import { ACTIVITY_PUB } from '../../../initializers/constants' +import { processActivities } from '../../activitypub/process/process' +import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler' async function process (payload: ActivityPubHttpPayload, jobId: number) { logger.info('Processing ActivityPub fetcher in job %d.', jobId) -- cgit v1.2.3