Federate video views

pull/128/head
Chocobozzz 2017-11-22 16:25:03 +01:00
parent c46edbc2f6
commit 40ff57078e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 188 additions and 44 deletions

View File

@ -1,14 +1,14 @@
import * as express from 'express'
import { Activity, ActivityAdd } from '../../../shared/models/activitypub/activity'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { pageToStartAndCount } from '../../helpers/core-utils'
import { database as db } from '../../initializers'
import { ACTIVITY_PUB } from '../../initializers/constants'
import { addActivityData } from '../../lib/activitypub/send/send-add'
import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
import { announceActivityData } from '../../lib/index'
import { asyncMiddleware, localAccountValidator } from '../../middlewares'
import { AccountInstance } from '../../models/account/account-interface'
import { pageToStartAndCount } from '../../helpers/core-utils'
import { ACTIVITY_PUB } from '../../initializers/constants'
const outboxRouter = express.Router()
@ -36,14 +36,18 @@ async function outboxController (req: express.Request, res: express.Response, ne
for (const video of data.data) {
const videoObject = video.toActivityPubObject()
let addActivity: ActivityAdd = await addActivityData(video.url, account, video, video.VideoChannel.url, videoObject)
// This is a shared video
if (video.VideoShare !== undefined) {
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const addActivity = await addActivityData(video.url, video.VideoChannel.Account, video, video.VideoChannel.url, videoObject)
const url = getAnnounceActivityPubUrl(video.url, account)
const announceActivity = await announceActivityData(url, account, addActivity)
activities.push(announceActivity)
} else {
const addActivity = await addActivityData(video.url, account, video, video.VideoChannel.url, videoObject)
activities.push(addActivity)
}
}

View File

@ -148,10 +148,17 @@ async function removeFollow (req: express.Request, res: express.Response, next:
const follow: AccountFollowInstance = res.locals.follow
await db.sequelize.transaction(async t => {
await sendUndoFollow(follow, t)
if (follow.state === 'accepted') await sendUndoFollow(follow, t)
await follow.destroy({ transaction: t })
})
// Destroy the account that will destroy video channels, videos and video files too
// This could be long so don't wait this task
const following = follow.AccountFollowing
following.destroy()
.catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.url, err))
return res.status(204).end()
}

View File

@ -11,10 +11,15 @@ import {
resetSequelizeInstance,
retryTransactionWrapper
} from '../../../helpers'
import { getServerAccount } from '../../../helpers/utils'
import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers'
import { database as db } from '../../../initializers/database'
import { sendAddVideo } from '../../../lib/activitypub/send/send-add'
import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update'
import { shareVideoByServer } from '../../../lib/activitypub/share'
import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
import { sendCreateViewToVideoFollowers } from '../../../lib/index'
import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler'
import {
asyncMiddleware,
@ -35,9 +40,7 @@ import { abuseVideoRouter } from './abuse'
import { blacklistRouter } from './blacklist'
import { videoChannelRouter } from './channel'
import { rateVideoRouter } from './rate'
import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
import { shareVideoByServer } from '../../../lib/activitypub/share'
import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
const videosRouter = express.Router()
@ -311,17 +314,18 @@ async function updateVideo (req: express.Request, res: express.Response) {
async function getVideo (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video
const baseIncrementPromise = videoInstance.increment('views')
.then(() => getServerAccount())
if (videoInstance.isOwned()) {
// The increment is done directly in the database, not using the instance value
// FIXME: make a real view system
// For example, only add a view when a user watch a video during 30s etc
videoInstance.increment('views')
.then(() => {
// TODO: send to followers a notification
})
.catch(err => logger.error('Cannot add view to video %s.', videoInstance.uuid, err))
baseIncrementPromise
.then(serverAccount => sendCreateViewToVideoFollowers(serverAccount, videoInstance, undefined))
.catch(err => logger.error('Cannot add view to video/send view to followers for %s.', videoInstance.uuid, err))
} else {
// TODO: send view event to followers
baseIncrementPromise
.then(serverAccount => sendCreateViewToOrigin(serverAccount, videoInstance, undefined))
.catch(err => logger.error('Cannot send view to origin server for %s.', videoInstance.uuid, err))
}
// Do not wait the view system

View File

@ -11,6 +11,7 @@ import {
isVideoTorrentDeleteActivityValid,
isVideoTorrentUpdateActivityValid
} from './videos'
import { isViewActivityValid } from './view'
function isRootActivityValid (activity: any) {
return Array.isArray(activity['@context']) &&
@ -55,7 +56,8 @@ export {
function checkCreateActivity (activity: any) {
return isVideoChannelCreateActivityValid(activity) ||
isVideoFlagValid(activity)
isVideoFlagValid(activity) ||
isViewActivityValid(activity)
}
function checkAddActivity (activity: any) {

View File

@ -5,3 +5,4 @@ export * from './signature'
export * from './undo'
export * from './video-channels'
export * from './videos'
export * from './view'

View File

@ -52,7 +52,7 @@ function isVideoTorrentObjectValid (video: any) {
setValidRemoteTags(video) &&
isRemoteIdentifierValid(video.category) &&
isRemoteIdentifierValid(video.licence) &&
isRemoteIdentifierValid(video.language) &&
(!video.language || isRemoteIdentifierValid(video.language)) &&
isVideoViewsValid(video.views) &&
isVideoNSFWValid(video.nsfw) &&
isDateValid(video.published) &&

View File

@ -0,0 +1,13 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isViewActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
activity.object.type === 'View' &&
isActivityPubUrlValid(activity.object.actor) &&
isActivityPubUrlValid(activity.object.object)
}
// ---------------------------------------------------------------------------
export {
isViewActivityValid
}

View File

@ -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,

View File

@ -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 ],

View File

@ -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

View File

@ -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
}

View File

@ -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 sendCreateView ()
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 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
}

View File

@ -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
}

View File

@ -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)

View File

@ -122,7 +122,7 @@ export interface VideoAttributes {
VideoChannel?: VideoChannelInstance
Tags?: TagInstance[]
VideoFiles?: VideoFileInstance[]
VideoShare?: VideoShareInstance
VideoShares?: VideoShareInstance[]
}
export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {

View File

@ -567,6 +567,14 @@ toActivityPubObject = function (this: VideoInstance) {
name: t.name
}))
let language
if (this.language) {
language = {
identifier: this.language + '',
name: this.getLanguageLabel()
}
}
const url = []
for (const file of this.VideoFiles) {
url.push({
@ -608,10 +616,7 @@ toActivityPubObject = function (this: VideoInstance) {
identifier: this.licence + '',
name: this.getLicenceLabel()
},
language: {
identifier: this.language + '',
name: this.getLanguageLabel()
},
language,
views: this.views,
nsfw: this.nsfw,
published: this.createdAt.toISOString(),
@ -816,7 +821,19 @@ listAllAndSharedByAccountForOutbox = function (accountId: number, start: number,
include: [
{
model: Video['sequelize'].models.VideoShare,
required: false
required: false,
where: {
[Sequelize.Op.and]: [
{
id: {
[Sequelize.Op.not]: null
}
},
{
accountId
}
]
}
},
{
model: Video['sequelize'].models.VideoChannel,

View File

@ -1,6 +1,7 @@
import { VideoChannelObject, VideoTorrentObject } from './objects'
import { ActivityPubSignature } from './activitypub-signature'
import { VideoChannelObject, VideoTorrentObject } from './objects'
import { VideoAbuseObject } from './objects/video-abuse-object'
import { ViewObject } from './objects/view-object'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
@ -20,7 +21,7 @@ export interface BaseActivity {
export interface ActivityCreate extends BaseActivity {
type: 'Create'
object: VideoChannelObject | VideoAbuseObject
object: VideoChannelObject | VideoAbuseObject | ViewObject
}
export interface ActivityAdd extends BaseActivity {

View File

@ -2,3 +2,4 @@ export * from './common-objects'
export * from './video-abuse-object'
export * from './video-channel-object'
export * from './video-torrent-object'
export * from './view-object'

View File

@ -0,0 +1,5 @@
export interface ViewObject {
type: 'View',
actor: string
object: string
}