Begin moving video channel to actor

This commit is contained in:
Chocobozzz 2017-12-14 17:38:41 +01:00
parent fadf619ad6
commit 50d6de9c28
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
100 changed files with 1761 additions and 2041 deletions

View File

@ -2,7 +2,7 @@ import { Component } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng' import { SortMeta } from 'primeng/primeng'
import { AccountFollow } from '../../../../../../shared/models/accounts/follow.model' import { AccountFollow } from '../../../../../../shared/models/actors/follow.model'
import { RestPagination, RestTable } from '../../../shared' import { RestPagination, RestTable } from '../../../shared'
import { FollowService } from '../shared' import { FollowService } from '../shared'

View File

@ -1,7 +1,7 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng' import { SortMeta } from 'primeng/primeng'
import { AccountFollow } from '../../../../../../shared/models/accounts/follow.model' import { AccountFollow } from '../../../../../../shared/models/actors/follow.model'
import { ConfirmService } from '../../../core/confirm/confirm.service' import { ConfirmService } from '../../../core/confirm/confirm.service'
import { RestPagination, RestTable } from '../../../shared' import { RestPagination, RestTable } from '../../../shared'
import { FollowService } from '../shared' import { FollowService } from '../shared'

View File

@ -10,7 +10,7 @@ import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject' import { ReplaySubject } from 'rxjs/ReplaySubject'
import { Subject } from 'rxjs/Subject' import { Subject } from 'rxjs/Subject'
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared' import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared'
import { Account } from '../../../../../shared/models/accounts' import { Account } from '../../../../../shared/models/actors'
import { UserLogin } from '../../../../../shared/models/users/user-login.model' import { UserLogin } from '../../../../../shared/models/users/user-login.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { RestExtractor } from '../../shared/rest' import { RestExtractor } from '../../shared/rest'

View File

@ -1,4 +1,4 @@
import { Account as ServerAccount } from '../../../../../shared/models/accounts/account.model' import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'

View File

@ -1,4 +1,4 @@
import { Account } from '../../../../../shared/models/accounts' import { Account } from '../../../../../shared/models/actors'
import { Video } from '../../shared/video/video.model' import { Video } from '../../shared/video/video.model'
import { AuthUser } from '../../core' import { AuthUser } from '../../core'
import { import {

View File

@ -1,6 +1,6 @@
import { User } from '../' import { User } from '../'
import { Video as VideoServerModel } from '../../../../../shared' import { Video as VideoServerModel } from '../../../../../shared'
import { Account } from '../../../../../shared/models/accounts' import { Account } from '../../../../../shared/models/actors'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {

View File

@ -1,14 +1,14 @@
import { getServerAccount } from '../server/helpers' import { getServerActor } from '../server/helpers'
import { initDatabaseModels } from '../server/initializers' import { initDatabaseModels } from '../server/initializers'
import { AccountFollowModel } from '../server/models/account/account-follow' import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
import { VideoModel } from '../server/models/video/video' import { VideoModel } from '../server/models/video/video'
initDatabaseModels(true) initDatabaseModels(true)
.then(() => { .then(() => {
return getServerAccount() return getServerActor()
}) })
.then(serverAccount => { .then(serverAccount => {
return AccountFollowModel.listAcceptedFollowingUrlsForApi([ serverAccount.id ], undefined) return ActorFollowModel.listAcceptedFollowingUrlsForApi([ serverAccount.id ], undefined)
}) })
.then(res => { .then(res => {
return res.total > 0 return res.total > 0

View File

@ -50,7 +50,8 @@ migrate()
// ----------- PeerTube modules ----------- // ----------- PeerTube modules -----------
import { installApplication } from './server/initializers' import { installApplication } from './server/initializers'
import { activitypubHttpJobScheduler, transcodingJobScheduler, VideosPreviewCache } from './server/lib' import { activitypubHttpJobScheduler, transcodingJobScheduler } from './server/lib/jobs'
import { VideosPreviewCache } from './server/lib/cache'
import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers' import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers'
// ----------- Command line ----------- // ----------- Command line -----------

View File

@ -2,20 +2,13 @@
import * as express from 'express' import * as express from 'express'
import { activityPubCollectionPagination, pageToStartAndCount } from '../../helpers' import { activityPubCollectionPagination, pageToStartAndCount } from '../../helpers'
import { ACTIVITY_PUB, CONFIG } from '../../initializers' import { ACTIVITY_PUB, CONFIG } from '../../initializers'
import { buildVideoChannelAnnounceToFollowers } from '../../lib/activitypub/send' import { buildVideoAnnounceToFollowers } from '../../lib/activitypub/send'
import { buildVideoAnnounceToFollowers } from '../../lib/index'
import { asyncMiddleware, executeIfActivityPub, localAccountValidator } from '../../middlewares' import { asyncMiddleware, executeIfActivityPub, localAccountValidator } from '../../middlewares'
import { import { videoChannelsGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
videoChannelsGetValidator,
videoChannelsShareValidator,
videosGetValidator,
videosShareValidator
} from '../../middlewares/validators'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { AccountFollowModel } from '../../models/account/account-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoChannelShareModel } from '../../models/video/video-channel-share'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
const activityPubClientRouter = express.Router() const activityPubClientRouter = express.Router()
@ -50,11 +43,6 @@ activityPubClientRouter.get('/video-channels/:id',
executeIfActivityPub(asyncMiddleware(videoChannelController)) executeIfActivityPub(asyncMiddleware(videoChannelController))
) )
activityPubClientRouter.get('/video-channels/:id/announces/:accountId',
executeIfActivityPub(asyncMiddleware(videoChannelsShareValidator)),
executeIfActivityPub(asyncMiddleware(videoChannelAnnounceController))
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -75,7 +63,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
const page = req.query.page || 1 const page = req.query.page || 1
const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
const result = await AccountFollowModel.listAcceptedFollowerUrlsForApi([ account.id ], undefined, start, count) const result = await ActorFollowModel.listAcceptedFollowerUrlsForApi([ account.Actor.id ], undefined, start, count)
const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result) const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result)
return res.json(activityPubResult) return res.json(activityPubResult)
@ -87,7 +75,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
const page = req.query.page || 1 const page = req.query.page || 1
const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
const result = await AccountFollowModel.listAcceptedFollowingUrlsForApi([ account.id ], undefined, start, count) const result = await ActorFollowModel.listAcceptedFollowingUrlsForApi([ account.Actor.id ], undefined, start, count)
const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result) const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result)
return res.json(activityPubResult) return res.json(activityPubResult)
@ -101,14 +89,7 @@ function videoController (req: express.Request, res: express.Response, next: exp
async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
const share = res.locals.videoShare as VideoShareModel const share = res.locals.videoShare as VideoShareModel
const object = await buildVideoAnnounceToFollowers(share.Account, res.locals.video, undefined) const object = await buildVideoAnnounceToFollowers(share.Actor, res.locals.video, undefined)
return res.json(object)
}
async function videoChannelAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
const share = res.locals.videoChannelShare as VideoChannelShareModel
const object = await buildVideoChannelAnnounceToFollowers(share.Account, share.VideoChannel, undefined)
return res.json(object) return res.json(object)
} }

View File

@ -5,6 +5,7 @@ import { isActivityValid } from '../../helpers/custom-validators/activitypub/act
import { processActivities } from '../../lib/activitypub/process/process' import { processActivities } from '../../lib/activitypub/process/process'
import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares' import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
import { ActorModel } from '../../models/activitypub/actor'
const inboxRouter = express.Router() const inboxRouter = express.Router()
@ -48,7 +49,14 @@ async function inboxController (req: express.Request, res: express.Response, nex
activities = activities.filter(a => isActivityValid(a)) activities = activities.filter(a => isActivityValid(a))
logger.debug('We keep %d activities.', activities.length, { activities }) logger.debug('We keep %d activities.', activities.length, { activities })
await processActivities(activities, res.locals.signature.account, res.locals.account) let specificActor: ActorModel = undefined
if (res.locals.account) {
specificActor = res.locals.account
} else if (res.locals.videoChannel) {
specificActor = res.locals.videoChannel
}
await processActivities(activities, res.locals.signature.actor, specificActor)
res.status(204).end() res.status(204).end()
} }

View File

@ -3,9 +3,8 @@ import { Activity } from '../../../shared/models/activitypub/activity'
import { activityPubCollectionPagination } from '../../helpers/activitypub' import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { pageToStartAndCount } from '../../helpers/core-utils' import { pageToStartAndCount } from '../../helpers/core-utils'
import { ACTIVITY_PUB } from '../../initializers/constants' import { ACTIVITY_PUB } from '../../initializers/constants'
import { addActivityData } from '../../lib/activitypub/send/send-add' import { announceActivityData, createActivityData } from '../../lib/activitypub/send'
import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
import { announceActivityData } from '../../lib/index'
import { asyncMiddleware, localAccountValidator } from '../../middlewares' import { asyncMiddleware, localAccountValidator } from '../../middlewares'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
@ -27,29 +26,30 @@ export {
async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) { async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
const account: AccountModel = res.locals.account const account: AccountModel = res.locals.account
const actor = account.Actor
const page = req.query.page || 1 const page = req.query.page || 1
const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
const data = await VideoModel.listAllAndSharedByAccountForOutbox(account.id, start, count) const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count)
const activities: Activity[] = [] const activities: Activity[] = []
for (const video of data.data) { for (const video of data.data) {
const videoObject = video.toActivityPubObject() const videoObject = video.toActivityPubObject()
// This is a shared video
const videoChannel = video.VideoChannel const videoChannel = video.VideoChannel
// This is a shared video
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.Actor.url, videoObject, undefined) const createActivity = await createActivityData(video.url, videoChannel.Account.Actor, videoObject, undefined)
const url = getAnnounceActivityPubUrl(video.url, account) const url = getAnnounceActivityPubUrl(video.url, actor)
const announceActivity = await announceActivityData(url, account, addActivity, undefined) const announceActivity = await announceActivityData(url, actor, createActivity, undefined)
activities.push(announceActivity) activities.push(announceActivity)
} else { } else {
const addActivity = await addActivityData(video.url, account, video, videoChannel.Actor.url, videoObject, undefined) const createActivity = await createActivityData(video.url, videoChannel.Account.Actor, videoObject, undefined)
activities.push(addActivity) activities.push(createActivity)
} }
} }
@ -57,7 +57,7 @@ async function outboxController (req: express.Request, res: express.Response, ne
data: activities, data: activities,
total: data.total total: data.total
} }
const json = activityPubCollectionPagination(account.url + '/outbox', page, newResult) const json = activityPubCollectionPagination(account.Actor.url + '/outbox', page, newResult)
return res.json(json).end() return res.json(json).end()
} }

View File

@ -1,10 +1,9 @@
import * as express from 'express' import * as express from 'express'
import { UserRight } from '../../../../shared/models/users' import { UserRight } from '../../../../shared/models/users'
import { getAccountFromWebfinger, getFormattedObjects, getServerAccount, logger, retryTransactionWrapper } from '../../../helpers' import { getFormattedObjects, getServerActor, loadActorUrlOrGetFromWebfinger, logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript, SERVER_ACCOUNT_NAME } from '../../../initializers' import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
import { saveAccountAndServerIfNotExist } from '../../../lib/activitypub' import { getOrCreateActorAndServerAndModel } from '../../../lib/activitypub'
import { sendUndoFollow } from '../../../lib/activitypub/send' import { sendFollow, sendUndoFollow } from '../../../lib/activitypub/send'
import { sendFollow } from '../../../lib/index'
import { import {
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
@ -17,8 +16,8 @@ import {
setPagination setPagination
} from '../../../middlewares' } from '../../../middlewares'
import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
const serverFollowsRouter = express.Router() const serverFollowsRouter = express.Router()
@ -38,7 +37,7 @@ serverFollowsRouter.post('/following',
asyncMiddleware(followRetry) asyncMiddleware(followRetry)
) )
serverFollowsRouter.delete('/following/:accountId', serverFollowsRouter.delete('/following/:host',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(removeFollowingValidator), asyncMiddleware(removeFollowingValidator),
@ -62,43 +61,41 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
const serverAccount = await getServerAccount() const serverActor = await getServerActor()
const resultList = await AccountFollowModel.listFollowingForApi(serverAccount.id, req.query.start, req.query.count, req.query.sort) const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
const serverAccount = await getServerAccount() const serverActor = await getServerActor()
const resultList = await AccountFollowModel.listFollowersForApi(serverAccount.id, req.query.start, req.query.count, req.query.sort) const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
async function followRetry (req: express.Request, res: express.Response, next: express.NextFunction) { async function followRetry (req: express.Request, res: express.Response, next: express.NextFunction) {
const hosts = req.body.hosts as string[] const hosts = req.body.hosts as string[]
const fromAccount = await getServerAccount() const fromActor = await getServerActor()
const tasks: Promise<any>[] = [] const tasks: Promise<any>[] = []
const accountName = SERVER_ACCOUNT_NAME const actorName = SERVER_ACTOR_NAME
for (const host of hosts) { for (const host of hosts) {
// We process each host in a specific transaction // We process each host in a specific transaction
// First, we add the follow request in the database // First, we add the follow request in the database
// Then we send the follow request to other account // Then we send the follow request to other actor
const p = loadLocalOrGetAccountFromWebfinger(accountName, host) const p = loadActorUrlOrGetFromWebfinger(actorName, host)
.then(accountResult => { .then(actorUrl => getOrCreateActorAndServerAndModel(actorUrl))
let targetAccount = accountResult.account .then(targetActor => {
const options = { const options = {
arguments: [ fromAccount, targetAccount, accountResult.loadedFromDB ], arguments: [ fromActor, targetActor ],
errorMessage: 'Cannot follow with many retries.' errorMessage: 'Cannot follow with many retries.'
} }
return retryTransactionWrapper(follow, options) return retryTransactionWrapper(follow, options)
}) })
.catch(err => logger.warn('Cannot follow server %s.', `${accountName}@${host}`, err)) .catch(err => logger.warn('Cannot follow server %s.', host, err))
tasks.push(p) tasks.push(p)
} }
@ -110,42 +107,32 @@ async function followRetry (req: express.Request, res: express.Response, next: e
return res.status(204).end() return res.status(204).end()
} }
async function follow (fromAccount: AccountModel, targetAccount: AccountModel, targetAlreadyInDB: boolean) { function follow (fromActor: ActorModel, targetActor: ActorModel) {
try { return sequelizeTypescript.transaction(async t => {
await sequelizeTypescript.transaction(async t => { const [ actorFollow ] = await ActorFollowModel.findOrCreate({
if (targetAlreadyInDB === false) { where: {
await saveAccountAndServerIfNotExist(targetAccount, t) actorId: fromActor.id,
} targetActorId: targetActor.id
},
const [ accountFollow ] = await AccountFollowModel.findOrCreate({ defaults: {
where: { state: 'pending',
accountId: fromAccount.id, actorId: fromActor.id,
targetAccountId: targetAccount.id targetActorId: targetActor.id
}, },
defaults: { transaction: t
state: 'pending',
accountId: fromAccount.id,
targetAccountId: targetAccount.id
},
transaction: t
})
accountFollow.AccountFollowing = targetAccount
accountFollow.AccountFollower = fromAccount
// Send a notification to remote server
if (accountFollow.state === 'pending') {
await sendFollow(accountFollow, t)
}
}) })
} catch (err) { actorFollow.ActorFollowing = targetActor
// Reset target account actorFollow.ActorFollower = fromActor
targetAccount.isNewRecord = !targetAlreadyInDB
throw err // Send a notification to remote server
} if (actorFollow.state === 'pending') {
await sendFollow(actorFollow, t)
}
})
} }
async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) { async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) {
const follow: AccountFollowModel = res.locals.follow const follow: ActorFollowModel = res.locals.follow
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
if (follow.state === 'accepted') await sendUndoFollow(follow, t) if (follow.state === 'accepted') await sendUndoFollow(follow, t)
@ -153,24 +140,11 @@ async function removeFollow (req: express.Request, res: express.Response, next:
await follow.destroy({ transaction: t }) await follow.destroy({ transaction: t })
}) })
// Destroy the account that will destroy video channels, videos and video files too // Destroy the actor that will destroy video channels, videos and video files too
// This could be long so don't wait this task // This could be long so don't wait this task
const following = follow.AccountFollowing const following = follow.ActorFollowing
following.destroy() following.destroy()
.catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.Actor.url, err)) .catch(err => logger.error('Cannot destroy actor that we do not follow anymore %s.', following.url, err))
return res.status(204).end() return res.status(204).end()
} }
async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) {
let loadedFromDB = true
let account = await AccountModel.loadByNameAndHost(name, host)
if (!account) {
const nameWithDomain = name + '@' + host
account = await getAccountFromWebfinger(nameWithDomain)
loadedFromDB = false
}
return { account, loadedFromDB }
}

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
import { getFormattedObjects, logger, retryTransactionWrapper } from '../../helpers' import { getFormattedObjects, logger, retryTransactionWrapper } from '../../helpers'
import { CONFIG } from '../../initializers' import { CONFIG } from '../../initializers'
import { createUserAccountAndChannel } from '../../lib' import { createUserAccountAndChannel } from '../../lib/user'
import { import {
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,

View File

@ -1,22 +1,18 @@
import * as express from 'express' import * as express from 'express'
import { import { UserRight, VideoAbuseCreate } from '../../../../shared'
logger, import { getFormattedObjects, logger, retryTransactionWrapper } from '../../../helpers'
getFormattedObjects,
retryTransactionWrapper
} from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { sendVideoAbuse } from '../../../lib/activitypub/send'
import { import {
asyncMiddleware,
authenticate, authenticate,
ensureUserHasRight, ensureUserHasRight,
paginationValidator, paginationValidator,
videoAbuseReportValidator,
videoAbusesSortValidator,
setVideoAbusesSort,
setPagination, setPagination,
asyncMiddleware setVideoAbusesSort,
videoAbuseReportValidator,
videoAbusesSortValidator
} from '../../../middlewares' } from '../../../middlewares'
import { VideoAbuseCreate, UserRight } from '../../../../shared'
import { sendVideoAbuse } from '../../../lib/index'
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 { VideoAbuseModel } from '../../../models/video/video-abuse' import { VideoAbuseModel } from '../../../models/video/video-abuse'
@ -80,7 +76,7 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
// We send the video abuse to the origin server // We send the video abuse to the origin server
if (videoInstance.isOwned() === false) { if (videoInstance.isOwned() === false) {
await sendVideoAbuse(reporterAccount, videoAbuseInstance, videoInstance, t) await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
} }
}) })

View File

@ -2,8 +2,8 @@ import * as express from 'express'
import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared' import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared'
import { getFormattedObjects, logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers' import { getFormattedObjects, logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { createVideoChannel } from '../../../lib' import { setAsyncActorKeys } from '../../../lib/activitypub'
import { sendUpdateVideoChannel } from '../../../lib/activitypub/send/send-update' import { createVideoChannel } from '../../../lib/video-channel'
import { import {
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
@ -92,15 +92,17 @@ async function addVideoChannelRetryWrapper (req: express.Request, res: express.R
return res.type('json').status(204).end() return res.type('json').status(204).end()
} }
function addVideoChannel (req: express.Request, res: express.Response) { async function addVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInfo: VideoChannelCreate = req.body const videoChannelInfo: VideoChannelCreate = req.body
const account: AccountModel = res.locals.oauth.token.User.Account const account: AccountModel = res.locals.oauth.token.User.Account
return sequelizeTypescript.transaction(async t => { const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
const videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t) return createVideoChannel(videoChannelInfo, account, t)
logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid)
}) })
setAsyncActorKeys(videoChannelCreated.Actor)
logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid)
} }
async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@ -128,12 +130,13 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) await videoChannelInstance.save(sequelizeOptions)
await sendUpdateVideoChannel(videoChannelInstanceUpdated, t) // TODO
// await sendUpdateVideoChannel(videoChannelInstanceUpdated, t)
}) })
logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.uuid) logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
} catch (err) { } catch (err) {
logger.debug('Cannot update the video channel.', err) logger.debug('Cannot update the video channel.', err)
@ -160,11 +163,12 @@ async function removeVideoChannelRetryWrapper (req: express.Request, res: expres
async function removeVideoChannel (req: express.Request, res: express.Response) { async function removeVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInstance: VideoChannelModel = res.locals.videoChannel const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
await sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
await videoChannelInstance.destroy({ transaction: t }) await videoChannelInstance.destroy({ transaction: t })
logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
}) })
logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.uuid)
} }
async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -11,7 +11,7 @@ import {
resetSequelizeInstance, resetSequelizeInstance,
retryTransactionWrapper retryTransactionWrapper
} from '../../../helpers' } from '../../../helpers'
import { getServerAccount } from '../../../helpers/utils' import { getServerActor } from '../../../helpers/utils'
import { import {
CONFIG, CONFIG,
sequelizeTypescript, sequelizeTypescript,
@ -22,8 +22,7 @@ import {
VIDEO_PRIVACIES VIDEO_PRIVACIES
} from '../../../initializers' } from '../../../initializers'
import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServer } from '../../../lib/activitypub' import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServer } from '../../../lib/activitypub'
import { sendAddVideo, sendCreateViewToOrigin, sendUpdateVideo } from '../../../lib/activitypub/send' import { sendCreateVideo, sendCreateViewToOrigin, sendCreateViewToVideoFollowers, sendUpdateVideo } from '../../../lib/activitypub/send'
import { sendCreateViewToVideoFollowers } from '../../../lib/index'
import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler' import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler'
import { import {
asyncMiddleware, asyncMiddleware,
@ -248,7 +247,8 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
// Don't send video to remote servers, it is private // Don't send video to remote servers, it is private
if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
await sendAddVideo(video, t) await sendCreateVideo(video, t)
// TODO: share by video channel
await shareVideoByServer(video, t) await shareVideoByServer(video, t)
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
@ -304,7 +304,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
// Video is not private anymore, send a create action to remote servers // Video is not private anymore, send a create action to remote servers
if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) { if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
await sendAddVideo(videoInstanceUpdated, t) await sendCreateVideo(videoInstanceUpdated, t)
// TODO: Send by video channel
await shareVideoByServer(videoInstanceUpdated, t) await shareVideoByServer(videoInstanceUpdated, t)
} }
}) })
@ -330,7 +331,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video const videoInstance = res.locals.video
await videoInstance.increment('views') await videoInstance.increment('views')
const serverAccount = await getServerAccount() const serverAccount = await getServerActor()
if (videoInstance.isOwned()) { if (videoInstance.isOwned()) {
await sendCreateViewToVideoFollowers(serverAccount, videoInstance, undefined) await sendCreateViewToVideoFollowers(serverAccount, videoInstance, undefined)

View File

@ -1,11 +1,7 @@
import * as express from 'express'
import * as cors from 'cors' import * as cors from 'cors'
import { import * as express from 'express'
CONFIG, import { CONFIG, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
STATIC_MAX_AGE, import { VideosPreviewCache } from '../lib/cache'
STATIC_PATHS
} from '../initializers'
import { VideosPreviewCache } from '../lib'
import { asyncMiddleware } from '../middlewares' import { asyncMiddleware } from '../middlewares'
const staticRouter = express.Router() const staticRouter = express.Router()

View File

@ -1,7 +1,7 @@
import * as express from 'express' import * as express from 'express'
import { asyncMiddleware } from '../middlewares' import { asyncMiddleware } from '../middlewares'
import { webfingerValidator } from '../middlewares/validators' import { webfingerValidator } from '../middlewares/validators'
import { AccountModel } from '../models/account/account' import { ActorModel } from '../models/activitypub/actor'
const webfingerRouter = express.Router() const webfingerRouter = express.Router()
@ -19,16 +19,16 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function webfingerController (req: express.Request, res: express.Response, next: express.NextFunction) { function webfingerController (req: express.Request, res: express.Response, next: express.NextFunction) {
const account = res.locals.account as AccountModel const actor = res.locals.actor as ActorModel
const json = { const json = {
subject: req.query.resource, subject: req.query.resource,
aliases: [ account.Actor.url ], aliases: [ actor.url ],
links: [ links: [
{ {
rel: 'self', rel: 'self',
type: 'application/activity+json', type: 'application/activity+json',
href: account.Actor.url href: actor.url
} }
] ]
} }

View File

@ -1,7 +1,7 @@
import { ResultList } from '../../shared/models' import { ResultList } from '../../shared/models'
import { Activity } from '../../shared/models/activitypub' import { Activity } from '../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../initializers' import { ACTIVITY_PUB } from '../initializers'
import { AccountModel } from '../models/account/account' import { ActorModel } from '../models/activitypub/actor'
import { signObject } from './peertube-crypto' import { signObject } from './peertube-crypto'
function activityPubContextify <T> (data: T) { function activityPubContextify <T> (data: T) {
@ -71,10 +71,10 @@ function activityPubCollectionPagination (url: string, page: any, result: Result
return orderedCollectionPagination return orderedCollectionPagination
} }
function buildSignedActivity (byAccount: AccountModel, data: Object) { function buildSignedActivity (byActor: ActorModel, data: Object) {
const activity = activityPubContextify(data) const activity = activityPubContextify(data)
return signObject(byAccount, activity) as Promise<Activity> return signObject(byActor, activity) as Promise<Activity>
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,14 +1,14 @@
import * as validator from 'validator' import * as validator from 'validator'
import { Activity, ActivityType } from '../../../../shared/models/activitypub' import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './actor' import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid } from './actor'
import { isAnnounceActivityValid } from './announce' import { isAnnounceActivityValid } from './announce'
import { isActivityPubUrlValid } from './misc' import { isActivityPubUrlValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isDislikeActivityValid, isLikeActivityValid } from './rate'
import { isUndoActivityValid } from './undo' import { isUndoActivityValid } from './undo'
import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
import { import {
isVideoFlagValid, isVideoFlagValid,
isVideoTorrentAddActivityValid, isVideoTorrentCreateActivityValid,
isVideoTorrentDeleteActivityValid, isVideoTorrentDeleteActivityValid,
isVideoTorrentUpdateActivityValid isVideoTorrentUpdateActivityValid
} from './videos' } from './videos'
@ -29,7 +29,6 @@ function isRootActivityValid (activity: any) {
const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
Create: checkCreateActivity, Create: checkCreateActivity,
Add: checkAddActivity,
Update: checkUpdateActivity, Update: checkUpdateActivity,
Delete: checkDeleteActivity, Delete: checkDeleteActivity,
Follow: checkFollowActivity, Follow: checkFollowActivity,
@ -59,14 +58,10 @@ export {
function checkCreateActivity (activity: any) { function checkCreateActivity (activity: any) {
return isViewActivityValid(activity) || return isViewActivityValid(activity) ||
isDislikeActivityValid(activity) || isDislikeActivityValid(activity) ||
isVideoChannelCreateActivityValid(activity) || isVideoTorrentCreateActivityValid(activity) ||
isVideoFlagValid(activity) isVideoFlagValid(activity)
} }
function checkAddActivity (activity: any) {
return isVideoTorrentAddActivityValid(activity)
}
function checkUpdateActivity (activity: any) { function checkUpdateActivity (activity: any) {
return isVideoTorrentUpdateActivityValid(activity) || return isVideoTorrentUpdateActivityValid(activity) ||
isVideoChannelUpdateActivityValid(activity) isVideoChannelUpdateActivityValid(activity)
@ -75,15 +70,15 @@ function checkUpdateActivity (activity: any) {
function checkDeleteActivity (activity: any) { function checkDeleteActivity (activity: any) {
return isVideoTorrentDeleteActivityValid(activity) || return isVideoTorrentDeleteActivityValid(activity) ||
isVideoChannelDeleteActivityValid(activity) || isVideoChannelDeleteActivityValid(activity) ||
isAccountDeleteActivityValid(activity) isActorDeleteActivityValid(activity)
} }
function checkFollowActivity (activity: any) { function checkFollowActivity (activity: any) {
return isAccountFollowActivityValid(activity) return isActorFollowActivityValid(activity)
} }
function checkAcceptActivity (activity: any) { function checkAcceptActivity (activity: any) {
return isAccountAcceptActivityValid(activity) return isActorAcceptActivityValid(activity)
} }
function checkAnnounceActivity (activity: any) { function checkAnnounceActivity (activity: any) {

View File

@ -1,8 +1,12 @@
import * as Bluebird from 'bluebird'
import { Response } from 'express'
import * as validator from 'validator' import * as validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../../initializers' import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
import { isAccountNameValid } from '../accounts' import { isAccountNameValid } from '../accounts'
import { exists, isUUIDValid } from '../misc' import { exists, isUUIDValid } from '../misc'
import { isActivityPubUrlValid, isBaseActivityValid } from './misc' import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
function isActorEndpointsObjectValid (endpointObject: any) { function isActorEndpointsObjectValid (endpointObject: any) {
return isActivityPubUrlValid(endpointObject.sharedInbox) return isActivityPubUrlValid(endpointObject.sharedInbox)
@ -27,7 +31,12 @@ function isActorPublicKeyValid (publicKey: string) {
} }
function isActorPreferredUsernameValid (preferredUsername: string) { function isActorPreferredUsernameValid (preferredUsername: string) {
return isAccountNameValid(preferredUsername) return isAccountNameValid(preferredUsername) || isVideoChannelNameValid(preferredUsername)
}
const actorNameRegExp = new RegExp('[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]+')
function isActorNameValid (name: string) {
return exists(name) && validator.matches(name, actorNameRegExp)
} }
function isActorPrivateKeyValid (privateKey: string) { function isActorPrivateKeyValid (privateKey: string) {
@ -46,10 +55,16 @@ function isRemoteActorValid (remoteActor: any) {
isActivityPubUrlValid(remoteActor.followers) && isActivityPubUrlValid(remoteActor.followers) &&
isActivityPubUrlValid(remoteActor.inbox) && isActivityPubUrlValid(remoteActor.inbox) &&
isActivityPubUrlValid(remoteActor.outbox) && isActivityPubUrlValid(remoteActor.outbox) &&
isActorNameValid(remoteActor.name) &&
isActorPreferredUsernameValid(remoteActor.preferredUsername) && isActorPreferredUsernameValid(remoteActor.preferredUsername) &&
isActivityPubUrlValid(remoteActor.url) && isActivityPubUrlValid(remoteActor.url) &&
isActorPublicKeyObjectValid(remoteActor.publicKey) && isActorPublicKeyObjectValid(remoteActor.publicKey) &&
isActorEndpointsObjectValid(remoteActor.endpoints) isActorEndpointsObjectValid(remoteActor.endpoints) &&
(!remoteActor.summary || isVideoChannelDescriptionValid(remoteActor.summary)) &&
setValidAttributedTo(remoteActor) &&
// If this is not an account, it should be attributed to an account
// In PeerTube we use this to attach a video channel to a specific account
(remoteActor.type === 'Person' || remoteActor.attributedTo.length !== 0)
} }
function isActorFollowingCountValid (value: string) { function isActorFollowingCountValid (value: string) {
@ -73,6 +88,40 @@ function isActorAcceptActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Accept') return isBaseActivityValid(activity, 'Accept')
} }
function isActorIdExist (id: number | string, res: Response) {
let promise: Bluebird<ActorModel>
if (validator.isInt('' + id)) {
promise = ActorModel.load(+id)
} else { // UUID
promise = ActorModel.loadByUUID('' + id)
}
return isActorExist(promise, res)
}
function isLocalActorNameExist (name: string, res: Response) {
const promise = ActorModel.loadLocalByName(name)
return isActorExist(promise, res)
}
async function isActorExist (p: Bluebird<ActorModel>, res: Response) {
const actor = await p
if (!actor) {
res.status(404)
.send({ error: 'Actor not found' })
.end()
return false
}
res.locals.actor = actor
return true
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -87,5 +136,9 @@ export {
isActorFollowersCountValid, isActorFollowersCountValid,
isActorFollowActivityValid, isActorFollowActivityValid,
isActorAcceptActivityValid, isActorAcceptActivityValid,
isActorDeleteActivityValid isActorDeleteActivityValid,
isActorIdExist,
isLocalActorNameExist,
isActorNameValid,
isActorExist
} }

View File

@ -1,12 +1,10 @@
import { isBaseActivityValid } from './misc' import { isBaseActivityValid } from './misc'
import { isVideoTorrentAddActivityValid } from './videos' import { isVideoTorrentCreateActivityValid } from './videos'
import { isVideoChannelCreateActivityValid } from './video-channels'
function isAnnounceActivityValid (activity: any) { function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') && return isBaseActivityValid(activity, 'Announce') &&
( (
isVideoChannelCreateActivityValid(activity.object) || isVideoTorrentCreateActivityValid(activity.object)
isVideoTorrentAddActivityValid(activity.object)
) )
} }

View File

@ -17,7 +17,7 @@ function isActivityPubUrlValid (url: string) {
isURLOptions.require_tld = false isURLOptions.require_tld = false
} }
return exists(url) && validator.isURL(url, isURLOptions) && validator.isLength(url, CONSTRAINTS_FIELDS.ACCOUNTS.URL) return exists(url) && validator.isURL(url, isURLOptions) && validator.isLength(url, CONSTRAINTS_FIELDS.ACTOR.URL)
} }
function isBaseActivityValid (activity: any, type: string) { function isBaseActivityValid (activity: any, type: string) {
@ -35,7 +35,23 @@ function isBaseActivityValid (activity: any, type: string) {
) )
} }
function setValidAttributedTo (obj: any) {
if (Array.isArray(obj.attributedTo) === false) {
obj.attributedTo = []
return true
}
const newAttributesTo = obj.attributedTo.filter(a => {
return (a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id)
})
obj.attributedTo = newAttributesTo
return true
}
export { export {
isActivityPubUrlValid, isActivityPubUrlValid,
isBaseActivityValid isBaseActivityValid,
setValidAttributedTo
} }

View File

@ -1,11 +1,11 @@
import { isAccountFollowActivityValid } from './actor' import { isActorFollowActivityValid } from './actor'
import { isBaseActivityValid } from './misc' import { isBaseActivityValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isDislikeActivityValid, isLikeActivityValid } from './rate'
function isUndoActivityValid (activity: any) { function isUndoActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Undo') && return isBaseActivityValid(activity, 'Undo') &&
( (
isAccountFollowActivityValid(activity.object) || isActorFollowActivityValid(activity.object) ||
isLikeActivityValid(activity.object) || isLikeActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) isDislikeActivityValid(activity.object)
) )

View File

@ -2,11 +2,6 @@ import { isDateValid, isUUIDValid } from '../misc'
import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
import { isActivityPubUrlValid, isBaseActivityValid } from './misc' import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isVideoChannelCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
isVideoChannelObjectValid(activity.object)
}
function isVideoChannelUpdateActivityValid (activity: any) { function isVideoChannelUpdateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Update') && return isBaseActivityValid(activity, 'Update') &&
isVideoChannelObjectValid(activity.object) isVideoChannelObjectValid(activity.object)
@ -29,7 +24,6 @@ function isVideoChannelObjectValid (videoChannel: any) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
isVideoChannelCreateActivityValid,
isVideoChannelUpdateActivityValid, isVideoChannelUpdateActivityValid,
isVideoChannelDeleteActivityValid, isVideoChannelDeleteActivityValid,
isVideoChannelObjectValid isVideoChannelObjectValid

View File

@ -10,10 +10,10 @@ import {
isVideoTruncatedDescriptionValid, isVideoTruncatedDescriptionValid,
isVideoViewsValid isVideoViewsValid
} from '../videos' } from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
function isVideoTorrentAddActivityValid (activity: any) { function isVideoTorrentCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Add') && return isBaseActivityValid(activity, 'Create') &&
isVideoTorrentObjectValid(activity.object) isVideoTorrentObjectValid(activity.object)
} }
@ -43,6 +43,8 @@ function isActivityPubVideoDurationValid (value: string) {
} }
function isVideoTorrentObjectValid (video: any) { function isVideoTorrentObjectValid (video: any) {
console.log(video)
return video.type === 'Video' && return video.type === 'Video' &&
isActivityPubUrlValid(video.id) && isActivityPubUrlValid(video.id) &&
isVideoNameValid(video.name) && isVideoNameValid(video.name) &&
@ -59,13 +61,15 @@ function isVideoTorrentObjectValid (video: any) {
(!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
isRemoteVideoIconValid(video.icon) && isRemoteVideoIconValid(video.icon) &&
setValidRemoteVideoUrls(video) && setValidRemoteVideoUrls(video) &&
video.url.length !== 0 video.url.length !== 0 &&
setValidAttributedTo(video) &&
video.attributedTo.length !== 0
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
isVideoTorrentAddActivityValid, isVideoTorrentCreateActivityValid,
isVideoTorrentUpdateActivityValid, isVideoTorrentUpdateActivityValid,
isVideoTorrentDeleteActivityValid, isVideoTorrentDeleteActivityValid,
isVideoFlagValid isVideoFlagValid

View File

@ -5,11 +5,11 @@ function isWebfingerResourceValid (value: string) {
if (!exists(value)) return false if (!exists(value)) return false
if (value.startsWith('acct:') === false) return false if (value.startsWith('acct:') === false) return false
const accountWithHost = value.substr(5) const actorWithHost = value.substr(5)
const accountParts = accountWithHost.split('@') const actorParts = actorWithHost.split('@')
if (accountParts.length !== 2) return false if (actorParts.length !== 2) return false
const host = accountParts[1] const host = actorParts[1]
return host === CONFIG.WEBSERVER.HOST return host === CONFIG.WEBSERVER.HOST
} }

View File

@ -1,5 +1,5 @@
import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
import { AccountModel } from '../models/account/account' import { ActorModel } from '../models/activitypub/actor'
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
import { jsig } from './custom-jsonld-signature' import { jsig } from './custom-jsonld-signature'
import { logger } from './logger' import { logger } from './logger'
@ -13,18 +13,18 @@ async function createPrivateAndPublicKeys () {
return { privateKey: key, publicKey } return { privateKey: key, publicKey }
} }
function isSignatureVerified (fromAccount: AccountModel, signedDocument: object) { function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
const publicKeyObject = { const publicKeyObject = {
'@context': jsig.SECURITY_CONTEXT_URL, '@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromAccount.url, '@id': fromActor.url,
'@type': 'CryptographicKey', '@type': 'CryptographicKey',
owner: fromAccount.url, owner: fromActor.url,
publicKeyPem: fromAccount.publicKey publicKeyPem: fromActor.publicKey
} }
const publicKeyOwnerObject = { const publicKeyOwnerObject = {
'@context': jsig.SECURITY_CONTEXT_URL, '@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromAccount.url, '@id': fromActor.url,
publicKey: [ publicKeyObject ] publicKey: [ publicKeyObject ]
} }
@ -40,10 +40,10 @@ function isSignatureVerified (fromAccount: AccountModel, signedDocument: object)
}) })
} }
function signObject (byAccount: AccountModel, data: any) { function signObject (byActor: ActorModel, data: any) {
const options = { const options = {
privateKeyPem: byAccount.privateKey, privateKeyPem: byActor.privateKey,
creator: byAccount.url creator: byActor.url
} }
return jsig.promises.sign(data, options) return jsig.promises.sign(data, options)

View File

@ -3,8 +3,9 @@ import { Model } from 'sequelize-typescript'
import { ResultList } from '../../shared' import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos' import { VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers' import { CONFIG } from '../initializers'
import { AccountModel } from '../models/account/account'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor'
import { ApplicationModel } from '../models/application/application'
import { pseudoRandomBytesPromise } from './core-utils' import { pseudoRandomBytesPromise } from './core-utils'
import { logger } from './logger' import { logger } from './logger'
@ -80,18 +81,19 @@ function resetSequelizeInstance (instance: Model<any>, savedFields: object) {
}) })
} }
let serverAccount: AccountModel let serverActor: ActorModel
async function getServerAccount () { async function getServerActor () {
if (serverAccount === undefined) { if (serverActor === undefined) {
serverAccount = await AccountModel.loadApplication() const application = await ApplicationModel.load()
serverActor = application.Account.Actor
} }
if (!serverAccount) { if (!serverActor) {
logger.error('Cannot load server account.') logger.error('Cannot load server actor.')
process.exit(0) process.exit(0)
} }
return Promise.resolve(serverAccount) return Promise.resolve(serverActor)
} }
type SortType = { sortModel: any, sortValue: string } type SortType = { sortModel: any, sortValue: string }
@ -105,6 +107,6 @@ export {
isSignupAllowed, isSignupAllowed,
computeResolutionsToTranscode, computeResolutionsToTranscode,
resetSequelizeInstance, resetSequelizeInstance,
getServerAccount, getServerActor,
SortType SortType
} }

View File

@ -1,6 +1,6 @@
import * as WebFinger from 'webfinger.js' import * as WebFinger from 'webfinger.js'
import { WebFingerData } from '../../shared' import { WebFingerData } from '../../shared'
import { fetchRemoteAccount } from '../lib/activitypub' import { ActorModel } from '../models/activitypub/actor'
import { isTestInstance } from './core-utils' import { isTestInstance } from './core-utils'
import { isActivityPubUrlValid } from './custom-validators/activitypub' import { isActivityPubUrlValid } from './custom-validators/activitypub'
@ -11,9 +11,23 @@ const webfinger = new WebFinger({
request_timeout: 3000 request_timeout: 3000
}) })
async function getAccountFromWebfinger (nameWithHost: string) { async function loadActorUrlOrGetFromWebfinger (name: string, host: string) {
const webfingerData: WebFingerData = await webfingerLookup(nameWithHost) const actor = await ActorModel.loadByNameAndHost(name, host)
if (actor) return actor.url
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
return getLinkOrThrow(webfingerData)
}
// ---------------------------------------------------------------------------
export {
loadActorUrlOrGetFromWebfinger
}
// ---------------------------------------------------------------------------
function getLinkOrThrow (webfingerData: WebFingerData) {
if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.') if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.')
const selfLink = webfingerData.links.find(l => l.rel === 'self') const selfLink = webfingerData.links.find(l => l.rel === 'self')
@ -21,20 +35,9 @@ async function getAccountFromWebfinger (nameWithHost: string) {
throw new Error('Cannot find self link or href is not a valid URL.') throw new Error('Cannot find self link or href is not a valid URL.')
} }
const account = await fetchRemoteAccount(selfLink.href) return selfLink.href
if (account === undefined) throw new Error('Cannot fetch remote account ' + selfLink.href)
return account
} }
// ---------------------------------------------------------------------------
export {
getAccountFromWebfinger
}
// ---------------------------------------------------------------------------
function webfingerLookup (nameWithHost: string) { function webfingerLookup (nameWithHost: string) {
return new Promise<WebFingerData>((res, rej) => { return new Promise<WebFingerData>((res, rej) => {
webfinger.lookup(nameWithHost, (err, p) => { webfinger.lookup(nameWithHost, (err, p) => {

View File

@ -1,7 +1,8 @@
import * as config from 'config' import * as config from 'config'
import { join } from 'path' import { join } from 'path'
import { JobCategory, JobState, VideoRateType } from '../../shared/models' import { JobCategory, JobState, VideoRateType } from '../../shared/models'
import { FollowState } from '../../shared/models/accounts' import { FollowState } from '../../shared/models/actors'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible // Do not use barrels, remain constants as independent as possible
import { isTestInstance, root } from '../helpers/core-utils' import { isTestInstance, root } from '../helpers/core-utils'
@ -210,7 +211,7 @@ const VIDEO_MIMETYPE_EXT = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const SERVER_ACCOUNT_NAME = 'peertube' const SERVER_ACTOR_NAME = 'peertube'
const ACTIVITY_PUB = { const ACTIVITY_PUB = {
POTENTIAL_ACCEPT_HEADERS: [ POTENTIAL_ACCEPT_HEADERS: [
@ -229,6 +230,12 @@ const ACTIVITY_PUB = {
} }
} }
const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
GROUP: 'Group',
PERSON: 'Person',
APPLICATION: 'Application'
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Number of points we add/remove from a friend after a successful/bad request // Number of points we add/remove from a friend after a successful/bad request
@ -350,12 +357,13 @@ export {
REMOTE_SCHEME, REMOTE_SCHEME,
FOLLOW_STATES, FOLLOW_STATES,
AVATARS_DIR, AVATARS_DIR,
SERVER_ACCOUNT_NAME, SERVER_ACTOR_NAME,
PRIVATE_RSA_KEY_SIZE, PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS, SORTABLE_COLUMNS,
STATIC_MAX_AGE, STATIC_MAX_AGE,
STATIC_PATHS, STATIC_PATHS,
ACTIVITY_PUB, ACTIVITY_PUB,
ACTIVITY_PUB_ACTOR_TYPES,
THUMBNAILS_SIZE, THUMBNAILS_SIZE,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,

View File

@ -3,9 +3,10 @@ import { isTestInstance } from '../helpers/core-utils'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { AccountFollowModel } from '../models/account/account-follow'
import { AccountVideoRateModel } from '../models/account/account-video-rate' import { AccountVideoRateModel } from '../models/account/account-video-rate'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor'
import { ActorFollowModel } from '../models/activitypub/actor-follow'
import { ApplicationModel } from '../models/application/application' import { ApplicationModel } from '../models/application/application'
import { AvatarModel } from '../models/avatar/avatar' import { AvatarModel } from '../models/avatar/avatar'
import { JobModel } from '../models/job/job' import { JobModel } from '../models/job/job'
@ -17,7 +18,6 @@ import { VideoModel } from '../models/video/video'
import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoAbuseModel } from '../models/video/video-abuse'
import { VideoBlacklistModel } from '../models/video/video-blacklist' import { VideoBlacklistModel } from '../models/video/video-blacklist'
import { VideoChannelModel } from '../models/video/video-channel' import { VideoChannelModel } from '../models/video/video-channel'
import { VideoChannelShareModel } from '../models/video/video-channel-share'
import { VideoFileModel } from '../models/video/video-file' import { VideoFileModel } from '../models/video/video-file'
import { VideoShareModel } from '../models/video/video-share' import { VideoShareModel } from '../models/video/video-share'
import { VideoTagModel } from '../models/video/video-tag' import { VideoTagModel } from '../models/video/video-tag'
@ -56,6 +56,8 @@ const sequelizeTypescript = new SequelizeTypescript({
async function initDatabaseModels (silent: boolean) { async function initDatabaseModels (silent: boolean) {
sequelizeTypescript.addModels([ sequelizeTypescript.addModels([
ApplicationModel, ApplicationModel,
ActorModel,
ActorFollowModel,
AvatarModel, AvatarModel,
AccountModel, AccountModel,
JobModel, JobModel,
@ -64,11 +66,9 @@ async function initDatabaseModels (silent: boolean) {
ServerModel, ServerModel,
TagModel, TagModel,
AccountVideoRateModel, AccountVideoRateModel,
AccountFollowModel,
UserModel, UserModel,
VideoAbuseModel, VideoAbuseModel,
VideoChannelModel, VideoChannelModel,
VideoChannelShareModel,
VideoShareModel, VideoShareModel,
VideoFileModel, VideoFileModel,
VideoBlacklistModel, VideoBlacklistModel,

View File

@ -1,12 +1,12 @@
import * as passwordGenerator from 'password-generator' import * as passwordGenerator from 'password-generator'
import { UserRole } from '../../shared' import { UserRole } from '../../shared'
import { createPrivateAndPublicKeys, logger, mkdirpPromise, rimrafPromise } from '../helpers' import { logger, mkdirpPromise, rimrafPromise } from '../helpers'
import { createLocalAccountWithoutKeys, createUserAccountAndChannel } from '../lib' import { createApplicationActor, createUserAccountAndChannel } from '../lib/user'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ApplicationModel } from '../models/application/application' import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client' import { OAuthClientModel } from '../models/oauth/oauth-client'
import { applicationExist, clientsExist, usersExist } from './checker' import { applicationExist, clientsExist, usersExist } from './checker'
import { CACHE, CONFIG, LAST_MIGRATION_VERSION, SERVER_ACCOUNT_NAME } from './constants' import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants'
import { sequelizeTypescript } from './database' import { sequelizeTypescript } from './database'
async function installApplication () { async function installApplication () {
@ -134,15 +134,12 @@ async function createApplicationIfNotExist () {
if (exist === true) return undefined if (exist === true) return undefined
logger.info('Creating Application table.') logger.info('Creating Application table.')
const applicationInstance = await ApplicationModel.create({ migrationVersion: LAST_MIGRATION_VERSION })
logger.info('Creating application account.') logger.info('Creating application account.')
const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACCOUNT_NAME, null, applicationInstance.id, undefined) const application = await ApplicationModel.create({
migrationVersion: LAST_MIGRATION_VERSION
})
const { publicKey, privateKey } = await createPrivateAndPublicKeys() return createApplicationActor(application.id)
accountCreated.set('publicKey', publicKey)
accountCreated.set('privateKey', privateKey)
return accountCreated.save()
} }

View File

@ -5,7 +5,7 @@ import { shareVideoByServer } from '../../lib/activitypub/share'
import { getVideoActivityPubUrl, getVideoChannelActivityPubUrl } from '../../lib/activitypub/url' import { getVideoActivityPubUrl, getVideoChannelActivityPubUrl } from '../../lib/activitypub/url'
import { createLocalAccountWithoutKeys } from '../../lib/user' import { createLocalAccountWithoutKeys } from '../../lib/user'
import { ApplicationModel } from '../../models/application/application' import { ApplicationModel } from '../../models/application/application'
import { JOB_CATEGORIES, SERVER_ACCOUNT_NAME } from '../constants' import { JOB_CATEGORIES, SERVER_ACTOR_NAME } from '../constants'
async function up (utils: { async function up (utils: {
transaction: Sequelize.Transaction, transaction: Sequelize.Transaction,
@ -66,7 +66,7 @@ async function up (utils: {
// Create application account // Create application account
{ {
const applicationInstance = await ApplicationModel.findOne() const applicationInstance = await ApplicationModel.findOne()
const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACCOUNT_NAME, null, applicationInstance.id, undefined) const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined)
const { publicKey, privateKey } = await createPrivateAndPublicKeys() const { publicKey, privateKey } = await createPrivateAndPublicKeys()
accountCreated.set('publicKey', publicKey) accountCreated.set('publicKey', publicKey)

View File

@ -1,127 +0,0 @@
import * as Bluebird from 'bluebird'
import { Transaction } from 'sequelize'
import * as url from 'url'
import { ActivityPubActor } from '../../../shared/models/activitypub'
import { doRequest, logger, retryTransactionWrapper } from '../../helpers'
import { isRemoteAccountValid } from '../../helpers/custom-validators/activitypub'
import { ACTIVITY_PUB, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { ServerModel } from '../../models/server/server'
async function getOrCreateAccountAndServer (accountUrl: string) {
let account = await AccountModel.loadByUrl(accountUrl)
// We don't have this account in our database, fetch it on remote
if (!account) {
account = await fetchRemoteAccount(accountUrl)
if (account === undefined) throw new Error('Cannot fetch remote account.')
const options = {
arguments: [ account ],
errorMessage: 'Cannot save account and server with many retries.'
}
account = await retryTransactionWrapper(saveAccountAndServerIfNotExist, options)
}
return account
}
function saveAccountAndServerIfNotExist (account: AccountModel, t?: Transaction): Bluebird<AccountModel> | Promise<AccountModel> {
if (t !== undefined) {
return save(t)
} else {
return sequelizeTypescript.transaction(t => {
return save(t)
})
}
async function save (t: Transaction) {
const accountHost = url.parse(account.url).host
const serverOptions = {
where: {
host: accountHost
},
defaults: {
host: accountHost
},
transaction: t
}
const [ server ] = await ServerModel.findOrCreate(serverOptions)
// Save our new account in database
account.set('serverId', server.id)
account = await account.save({ transaction: t })
return account
}
}
async function fetchRemoteAccount (accountUrl: string) {
const options = {
uri: accountUrl,
method: 'GET',
headers: {
'Accept': ACTIVITY_PUB.ACCEPT_HEADER
}
}
logger.info('Fetching remote account %s.', accountUrl)
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warn('Cannot fetch remote account %s.', accountUrl, err)
return undefined
}
const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
if (isRemoteAccountValid(accountJSON) === false) {
logger.debug('Remote account JSON is not valid.', { accountJSON })
return undefined
}
const followersCount = await fetchAccountCount(accountJSON.followers)
const followingCount = await fetchAccountCount(accountJSON.following)
return new AccountModel({
uuid: accountJSON.uuid,
name: accountJSON.preferredUsername,
url: accountJSON.url,
publicKey: accountJSON.publicKey.publicKeyPem,
privateKey: null,
followersCount: followersCount,
followingCount: followingCount,
inboxUrl: accountJSON.inbox,
outboxUrl: accountJSON.outbox,
sharedInboxUrl: accountJSON.endpoints.sharedInbox,
followersUrl: accountJSON.followers,
followingUrl: accountJSON.following
})
}
export {
getOrCreateAccountAndServer,
fetchRemoteAccount,
saveAccountAndServerIfNotExist
}
// ---------------------------------------------------------------------------
async function fetchAccountCount (url: string) {
const options = {
uri: url,
method: 'GET'
}
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warn('Cannot fetch remote account count %s.', url, err)
return undefined
}
return requestResult.totalItems ? requestResult.totalItems : 0
}

View File

@ -0,0 +1,229 @@
import * as Bluebird from 'bluebird'
import { Transaction } from 'sequelize'
import * as url from 'url'
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
import { createPrivateAndPublicKeys, doRequest, logger, retryTransactionWrapper } from '../../helpers'
import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub'
import { ACTIVITY_PUB, CONFIG, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel'
// Set account keys, this could be long so process after the account creation and do not block the client
function setAsyncActorKeys (actor: ActorModel) {
return createPrivateAndPublicKeys()
.then(({ publicKey, privateKey }) => {
actor.set('publicKey', publicKey)
actor.set('privateKey', privateKey)
return actor.save()
})
.catch(err => {
logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
return actor
})
}
async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
let actor = await ActorModel.loadByUrl(actorUrl)
// We don't have this actor in our database, fetch it on remote
if (!actor) {
const result = await fetchRemoteActor(actorUrl)
if (result === undefined) throw new Error('Cannot fetch remote actor.')
// Create the attributed to actor
// In PeerTube a video channel is owned by an account
let ownerActor: ActorModel = undefined
if (recurseIfNeeded === true && result.actor.type === 'Group') {
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
try {
// Assert we don't recurse another time
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
} catch (err) {
logger.error('Cannot get or create account attributed to video channel ' + actor.url)
throw new Error(err)
}
}
const options = {
arguments: [ result, ownerActor ],
errorMessage: 'Cannot save actor and server with many retries.'
}
actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
}
return actor
}
function saveActorAndServerAndModelIfNotExist (
result: FetchRemoteActorResult,
ownerActor?: ActorModel,
t?: Transaction
): Bluebird<ActorModel> | Promise<ActorModel> {
let actor = result.actor
if (t !== undefined) return save(t)
return sequelizeTypescript.transaction(t => save(t))
async function save (t: Transaction) {
const actorHost = url.parse(actor.url).host
const serverOptions = {
where: {
host: actorHost
},
defaults: {
host: actorHost
},
transaction: t
}
const [ server ] = await ServerModel.findOrCreate(serverOptions)
// Save our new account in database
actor.set('serverId', server.id)
// Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
// (which could be false in a retried query)
const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
const account = await saveAccount(actorCreated, result, t)
actorCreated.Account = account
actorCreated.Account.Actor = actorCreated
} else if (actorCreated.type === 'Group') { // Video channel
const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
actorCreated.VideoChannel = videoChannel
actorCreated.VideoChannel.Actor = actorCreated
}
return actorCreated
}
}
type FetchRemoteActorResult = {
actor: ActorModel
preferredUsername: string
summary: string
attributedTo: ActivityPubAttributedTo[]
}
async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
const options = {
uri: actorUrl,
method: 'GET',
headers: {
'Accept': ACTIVITY_PUB.ACCEPT_HEADER
}
}
logger.info('Fetching remote actor %s.', actorUrl)
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warn('Cannot fetch remote actor %s.', actorUrl, err)
return undefined
}
const actorJSON: ActivityPubActor = JSON.parse(requestResult.body)
if (isRemoteActorValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
return undefined
}
const followersCount = await fetchActorTotalItems(actorJSON.followers)
const followingCount = await fetchActorTotalItems(actorJSON.following)
const actor = new ActorModel({
type: actorJSON.type,
uuid: actorJSON.uuid,
name: actorJSON.name,
url: actorJSON.url,
publicKey: actorJSON.publicKey.publicKeyPem,
privateKey: null,
followersCount: followersCount,
followingCount: followingCount,
inboxUrl: actorJSON.inbox,
outboxUrl: actorJSON.outbox,
sharedInboxUrl: actorJSON.endpoints.sharedInbox,
followersUrl: actorJSON.followers,
followingUrl: actorJSON.following
})
return {
actor,
preferredUsername: actorJSON.preferredUsername,
summary: actorJSON.summary,
attributedTo: actorJSON.attributedTo
}
}
function buildActorInstance (type: ActivityPubActorType, url: string, name: string, uuid?: string) {
return new ActorModel({
type,
url,
name,
uuid,
publicKey: null,
privateKey: null,
followersCount: 0,
followingCount: 0,
inboxUrl: url + '/inbox',
outboxUrl: url + '/outbox',
sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
followersUrl: url + '/followers',
followingUrl: url + '/following'
})
}
export {
getOrCreateActorAndServerAndModel,
saveActorAndServerAndModelIfNotExist,
fetchRemoteActor,
buildActorInstance,
setAsyncActorKeys
}
// ---------------------------------------------------------------------------
async function fetchActorTotalItems (url: string) {
const options = {
uri: url,
method: 'GET'
}
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warn('Cannot fetch remote actor count %s.', url, err)
return undefined
}
return requestResult.totalItems ? requestResult.totalItems : 0
}
function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
const account = new AccountModel({
name: result.preferredUsername,
actorId: actor.id
})
return account.save({ transaction: t })
}
async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
const videoChannel = new VideoChannelModel({
name: result.preferredUsername,
description: result.summary,
actorId: actor.id,
accountId: ownerActor.Account.id
})
return videoChannel.save({ transaction: t })
}

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { AccountModel } from '../../models/account/account' import { ActorModel } from '../../models/activitypub/actor'
import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../jobs/activitypub-http-job-scheduler' import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../jobs/activitypub-http-job-scheduler'
async function addFetchOutboxJob (account: AccountModel, t: Transaction) { async function addFetchOutboxJob (actor: ActorModel, t: Transaction) {
const jobPayload: ActivityPubHttpPayload = { const jobPayload: ActivityPubHttpPayload = {
uris: [ account.outboxUrl ] uris: [ actor.outboxUrl ]
} }
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpFetcherHandler', jobPayload) return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpFetcherHandler', jobPayload)

View File

@ -1,8 +1,7 @@
export * from './process' export * from './process'
export * from './send' export * from './send'
export * from './account' export * from './actor'
export * from './fetch' export * from './fetch'
export * from './share' export * from './share'
export * from './video-channels'
export * from './videos' export * from './videos'
export * from './url' export * from './url'

View File

@ -1,6 +1,5 @@
export * from './process' export * from './process'
export * from './process-accept' export * from './process-accept'
export * from './process-add'
export * from './process-announce' export * from './process-announce'
export * from './process-create' export * from './process-create'
export * from './process-delete' export * from './process-delete'

View File

@ -1,29 +1,13 @@
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { VideoTorrentObject } from '../../../../shared' import { VideoTorrentObject } from '../../../../shared'
import { VideoChannelObject } from '../../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../../shared/models/videos' import { VideoPrivacy } from '../../../../shared/models/videos'
import { doRequest } from '../../../helpers' import { doRequest } from '../../../helpers'
import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos' import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos'
import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers' import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel' import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountModel) {
return {
name: videoChannelObject.name,
description: videoChannelObject.content,
uuid: videoChannelObject.uuid,
url: videoChannelObject.id,
createdAt: new Date(videoChannelObject.published),
updatedAt: new Date(videoChannelObject.updated),
remote: true,
accountId: account.id
}
}
async function videoActivityObjectToDBAttributes ( async function videoActivityObjectToDBAttributes (
videoChannel: VideoChannelModel, videoChannel: VideoChannelModel,
@ -120,13 +104,13 @@ async function addVideoShares (instance: VideoModel, shares: string[]) {
uri: share, uri: share,
json: true json: true
}) })
const actor = json['actor'] const actorUrl = json['actor']
if (!actor) continue if (!actorUrl) continue
const account = await getOrCreateAccountAndServer(actor) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = { const entry = {
accountId: account.id, actorId: actor.id,
videoId: instance.id videoId: instance.id
} }
@ -137,36 +121,10 @@ async function addVideoShares (instance: VideoModel, shares: string[]) {
} }
} }
async function addVideoChannelShares (instance: VideoChannelModel, shares: string[]) {
for (const share of shares) {
// Fetch url
const json = await doRequest({
uri: share,
json: true
})
const actor = json['actor']
if (!actor) continue
const account = await getOrCreateAccountAndServer(actor)
const entry = {
accountId: account.id,
videoChannelId: instance.id
}
await VideoChannelShareModel.findOrCreate({
where: entry,
defaults: entry
})
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
videoFileActivityUrlToDBAttributes, videoFileActivityUrlToDBAttributes,
videoActivityObjectToDBAttributes, videoActivityObjectToDBAttributes,
videoChannelActivityObjectToDBAttributes,
addVideoChannelShares,
addVideoShares addVideoShares
} }

View File

@ -1,14 +1,14 @@
import { ActivityAccept } from '../../../../shared/models/activitypub' import { ActivityAccept } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { addFetchOutboxJob } from '../fetch' import { addFetchOutboxJob } from '../fetch'
async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountModel) { async function processAcceptActivity (activity: ActivityAccept, inboxActor?: ActorModel) {
if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.') if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.')
const targetAccount = await AccountModel.loadByUrl(activity.actor) const targetActor = await ActorModel.loadByUrl(activity.actor)
return processAccept(inboxAccount, targetAccount) return processAccept(inboxActor, targetActor)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -19,11 +19,11 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processAccept (account: AccountModel, targetAccount: AccountModel) { async function processAccept (actor: ActorModel, targetActor: ActorModel) {
const follow = await AccountFollowModel.loadByAccountAndTarget(account.id, targetAccount.id) const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id)
if (!follow) throw new Error('Cannot find associated follow.') if (!follow) throw new Error('Cannot find associated follow.')
follow.set('state', 'accepted') follow.set('state', 'accepted')
await follow.save() await follow.save()
await addFetchOutboxJob(targetAccount, undefined) await addFetchOutboxJob(targetActor, undefined)
} }

View File

@ -1,137 +0,0 @@
import * as Bluebird from 'bluebird'
import { VideoTorrentObject } from '../../../../shared'
import { ActivityAdd } from '../../../../shared/models/activitypub'
import { VideoRateType } from '../../../../shared/models/videos'
import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoFileModel } from '../../../models/video/video-file'
import { getOrCreateAccountAndServer } from '../account'
import { getOrCreateVideoChannel } from '../video-channels'
import { generateThumbnailFromUrl } from '../videos'
import { addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processAddActivity (activity: ActivityAdd) {
const activityObject = activity.object
const activityType = activityObject.type
const account = await getOrCreateAccountAndServer(activity.actor)
if (activityType === 'Video') {
const videoChannelUrl = activity.target
const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
return processAddVideo(account, activity, videoChannel, activityObject)
}
logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
return Promise.resolve(undefined)
}
// ---------------------------------------------------------------------------
export {
processAddActivity
}
// ---------------------------------------------------------------------------
async function processAddVideo (account: AccountModel,
activity: ActivityAdd,
videoChannel: VideoChannelModel,
videoToCreateData: VideoTorrentObject) {
const options = {
arguments: [ account, activity, videoChannel, videoToCreateData ],
errorMessage: 'Cannot insert the remote video with many retries.'
}
const video = await retryTransactionWrapper(addRemoteVideo, options)
// Process outside the transaction because we could fetch remote data
if (videoToCreateData.likes && Array.isArray(videoToCreateData.likes.orderedItems)) {
await createRates(videoToCreateData.likes.orderedItems, video, 'like')
}
if (videoToCreateData.dislikes && Array.isArray(videoToCreateData.dislikes.orderedItems)) {
await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike')
}
if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) {
await addVideoShares(video, videoToCreateData.shares.orderedItems)
}
return video
}
function addRemoteVideo (account: AccountModel,
activity: ActivityAdd,
videoChannel: VideoChannelModel,
videoToCreateData: VideoTorrentObject) {
logger.debug('Adding remote video %s.', videoToCreateData.id)
return sequelizeTypescript.transaction(async t => {
const sequelizeOptions = {
transaction: t
}
if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
const videoFromDatabase = await VideoModel.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t)
if (videoFromDatabase) return videoFromDatabase
const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc)
const video = VideoModel.build(videoData)
// Don't block on request
generateThumbnailFromUrl(video, videoToCreateData.icon)
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
const videoCreated = await video.save(sequelizeOptions)
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
if (videoFileAttributes.length === 0) {
throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
}
const tasks: Bluebird<any>[] = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
await Promise.all(tasks)
const tags = videoToCreateData.tag.map(t => t.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
return videoCreated
})
}
async function createRates (accountUrls: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0
const tasks: Bluebird<any>[] = []
for (const accountUrl of accountUrls) {
const account = await getOrCreateAccountAndServer(accountUrl)
const p = AccountVideoRateModel
.create({
videoId: video.id,
accountId: account.id,
type: rate
})
.then(() => rateCounts += 1)
tasks.push(p)
}
await Promise.all(tasks)
logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
// This is "likes" and "dislikes"
await video.increment(rate + 's', { by: rateCounts })
return
}

View File

@ -1,24 +1,19 @@
import { ActivityAdd, ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub' import { ActivityAnnounce } from '../../../../shared/models/activitypub'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc' import { forwardActivity } from '../send/misc'
import { processAddActivity } from './process-add'
import { processCreateActivity } from './process-create' import { processCreateActivity } from './process-create'
async function processAnnounceActivity (activity: ActivityAnnounce) { async function processAnnounceActivity (activity: ActivityAnnounce) {
const announcedActivity = activity.object const announcedActivity = activity.object
const accountAnnouncer = await getOrCreateAccountAndServer(activity.actor) const actorAnnouncer = await getOrCreateActorAndServerAndModel(activity.actor)
if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'Video') {
return processVideoChannelShare(accountAnnouncer, activity) return processVideoShare(actorAnnouncer, activity)
} else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
return processVideoShare(accountAnnouncer, activity)
} }
logger.warn( logger.warn(
@ -37,60 +32,24 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function processVideoChannelShare (accountAnnouncer: AccountModel, activity: ActivityAnnounce) { function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
const options = { const options = {
arguments: [ accountAnnouncer, activity ], arguments: [ actorAnnouncer, activity ],
errorMessage: 'Cannot share the video channel with many retries.'
}
return retryTransactionWrapper(shareVideoChannel, options)
}
async function shareVideoChannel (accountAnnouncer: AccountModel, activity: ActivityAnnounce) {
const announcedActivity = activity.object as ActivityCreate
return sequelizeTypescript.transaction(async t => {
// Add share entry
const videoChannel: VideoChannelModel = await processCreateActivity(announcedActivity)
const share = {
accountId: accountAnnouncer.id,
videoChannelId: videoChannel.id
}
const [ , created ] = await VideoChannelShareModel.findOrCreate({
where: share,
defaults: share,
transaction: t
})
if (videoChannel.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ accountAnnouncer ]
await forwardActivity(activity, t, exceptions)
}
return undefined
})
}
function processVideoShare (accountAnnouncer: AccountModel, activity: ActivityAnnounce) {
const options = {
arguments: [ accountAnnouncer, activity ],
errorMessage: 'Cannot share the video with many retries.' errorMessage: 'Cannot share the video with many retries.'
} }
return retryTransactionWrapper(shareVideo, options) return retryTransactionWrapper(shareVideo, options)
} }
function shareVideo (accountAnnouncer: AccountModel, activity: ActivityAnnounce) { function shareVideo (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
const announcedActivity = activity.object as ActivityAdd const announcedActivity = activity.object
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
// Add share entry // Add share entry
const video: VideoModel = await processAddActivity(announcedActivity) const video: VideoModel = await processCreateActivity(announcedActivity)
const share = { const share = {
accountId: accountAnnouncer.id, actorId: actorAnnouncer.id,
videoId: video.id videoId: video.id
} }
@ -102,7 +61,7 @@ function shareVideo (accountAnnouncer: AccountModel, activity: ActivityAnnounce)
if (video.isOwned() && created === true) { if (video.isOwned() && created === true) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ accountAnnouncer ] const exceptions = [ actorAnnouncer ]
await forwardActivity(activity, t, exceptions) await forwardActivity(activity, t, exceptions)
} }

View File

@ -1,30 +1,33 @@
import { ActivityCreate, VideoChannelObject } from '../../../../shared' import * as Bluebird from 'bluebird'
import { ActivityCreate, VideoTorrentObject } from '../../../../shared'
import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
import { VideoRateType } from '../../../../shared/models/videos'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { ActorModel } from '../../../models/activitypub/actor'
import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoAbuseModel } from '../../../models/video/video-abuse' import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { VideoChannelModel } from '../../../models/video/video-channel' import { VideoFileModel } from '../../../models/video/video-file'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc' import { forwardActivity } from '../send/misc'
import { getVideoChannelActivityPubUrl } from '../url' import { generateThumbnailFromUrl } from '../videos'
import { addVideoChannelShares, videoChannelActivityObjectToDBAttributes } from './misc' import { addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processCreateActivity (activity: ActivityCreate) { async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object const activityObject = activity.object
const activityType = activityObject.type const activityType = activityObject.type
const account = await getOrCreateAccountAndServer(activity.actor) const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (activityType === 'View') { if (activityType === 'View') {
return processCreateView(account, activity) return processCreateView(actor, activity)
} else if (activityType === 'Dislike') { } else if (activityType === 'Dislike') {
return processCreateDislike(account, activity) return processCreateDislike(actor, activity)
} else if (activityType === 'VideoChannel') { } else if (activityType === 'Video') {
return processCreateVideoChannel(account, activityObject as VideoChannelObject) return processCreateVideo(actor, activity)
} else if (activityType === 'Flag') { } else if (activityType === 'Flag') {
return processCreateVideoAbuse(account, activityObject as VideoAbuseObject) return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject)
} }
logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@ -39,17 +42,123 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processCreateDislike (byAccount: AccountModel, activity: ActivityCreate) { async function processCreateVideo (
actor: ActorModel,
activity: ActivityCreate
) {
const videoToCreateData = activity.object as VideoTorrentObject
const channel = videoToCreateData.attributedTo.find(a => a.type === 'Group')
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoToCreateData.url)
const channelActor = await getOrCreateActorAndServerAndModel(channel.id)
const options = { const options = {
arguments: [ byAccount, activity ], arguments: [ actor, activity, videoToCreateData, channelActor ],
errorMessage: 'Cannot insert the remote video with many retries.'
}
const video = await retryTransactionWrapper(createRemoteVideo, options)
// Process outside the transaction because we could fetch remote data
if (videoToCreateData.likes && Array.isArray(videoToCreateData.likes.orderedItems)) {
await createRates(videoToCreateData.likes.orderedItems, video, 'like')
}
if (videoToCreateData.dislikes && Array.isArray(videoToCreateData.dislikes.orderedItems)) {
await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike')
}
if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) {
await addVideoShares(video, videoToCreateData.shares.orderedItems)
}
return video
}
function createRemoteVideo (
account: ActorModel,
activity: ActivityCreate,
videoToCreateData: VideoTorrentObject,
channelActor: ActorModel
) {
logger.debug('Adding remote video %s.', videoToCreateData.id)
return sequelizeTypescript.transaction(async t => {
const sequelizeOptions = {
transaction: t
}
const videoFromDatabase = await VideoModel.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t)
if (videoFromDatabase) return videoFromDatabase
const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoToCreateData, activity.to, activity.cc)
const video = VideoModel.build(videoData)
// Don't block on request
generateThumbnailFromUrl(video, videoToCreateData.icon)
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
const videoCreated = await video.save(sequelizeOptions)
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
if (videoFileAttributes.length === 0) {
throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
}
const tasks: Bluebird<any>[] = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
await Promise.all(tasks)
const tags = videoToCreateData.tag.map(t => t.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
return videoCreated
})
}
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0
const tasks: Bluebird<any>[] = []
for (const actorUrl of actorUrls) {
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const p = AccountVideoRateModel
.create({
videoId: video.id,
accountId: actor.Account.id,
type: rate
})
.then(() => rateCounts += 1)
tasks.push(p)
}
await Promise.all(tasks)
logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
// This is "likes" and "dislikes"
await video.increment(rate + 's', { by: rateCounts })
return
}
async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) {
const options = {
arguments: [ byActor, 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: AccountModel, activity: ActivityCreate) { function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) {
const dislike = activity.object as DislikeObject const dislike = activity.object as DislikeObject
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t) const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t)
@ -69,20 +178,20 @@ function createVideoDislike (byAccount: AccountModel, activity: ActivityCreate)
if (video.isOwned() && created === true) { if (video.isOwned() && created === true) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byAccount ] const exceptions = [ byActor ]
await forwardActivity(activity, t, exceptions) await forwardActivity(activity, t, exceptions)
} }
}) })
} }
async function processCreateView (byAccount: AccountModel, activity: ActivityCreate) { async function processCreateView (byAccount: ActorModel, activity: ActivityCreate) {
const view = activity.object as ViewObject const view = activity.object as ViewObject
const video = await VideoModel.loadByUrlAndPopulateAccount(view.object) const video = await VideoModel.loadByUrlAndPopulateAccount(view.object)
if (!video) throw new Error('Unknown video ' + view.object) if (!video) throw new Error('Unknown video ' + view.object)
const account = await AccountModel.loadByUrl(view.actor) const account = await ActorModel.loadByUrl(view.actor)
if (!account) throw new Error('Unknown account ' + view.actor) if (!account) throw new Error('Unknown account ' + view.actor)
await video.increment('views') await video.increment('views')
@ -94,51 +203,21 @@ async function processCreateView (byAccount: AccountModel, activity: ActivityCre
} }
} }
async function processCreateVideoChannel (account: AccountModel, videoChannelToCreateData: VideoChannelObject) { function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) {
const options = { const options = {
arguments: [ account, videoChannelToCreateData ], arguments: [ actor, videoAbuseToCreateData ],
errorMessage: 'Cannot insert the remote video channel with many retries.'
}
const videoChannel = await retryTransactionWrapper(addRemoteVideoChannel, options)
if (videoChannelToCreateData.shares && Array.isArray(videoChannelToCreateData.shares.orderedItems)) {
await addVideoChannelShares(videoChannel, videoChannelToCreateData.shares.orderedItems)
}
return videoChannel
}
function addRemoteVideoChannel (account: AccountModel, videoChannelToCreateData: VideoChannelObject) {
logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
return sequelizeTypescript.transaction(async t => {
let videoChannel = await VideoChannelModel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
if (videoChannel) return videoChannel
const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
videoChannel = new VideoChannelModel(videoChannelData)
videoChannel.url = getVideoChannelActivityPubUrl(videoChannel)
videoChannel = await videoChannel.save({ transaction: t })
logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
return videoChannel
})
}
function processCreateVideoAbuse (account: AccountModel, videoAbuseToCreateData: VideoAbuseObject) {
const options = {
arguments: [ account, videoAbuseToCreateData ],
errorMessage: 'Cannot insert the remote video abuse with many retries.' errorMessage: 'Cannot insert the remote video abuse with many retries.'
} }
return retryTransactionWrapper(addRemoteVideoAbuse, options) return retryTransactionWrapper(addRemoteVideoAbuse, options)
} }
function addRemoteVideoAbuse (account: AccountModel, videoAbuseToCreateData: VideoAbuseObject) { function addRemoteVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) {
logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
const account = actor.Account
if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url)
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t) const video = await VideoModel.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
if (!video) { if (!video) {

View File

@ -2,28 +2,30 @@ import { ActivityDelete } from '../../../../shared/models/activitypub'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel' import { VideoChannelModel } from '../../../models/video/video-channel'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
async function processDeleteActivity (activity: ActivityDelete) { async function processDeleteActivity (activity: ActivityDelete) {
const account = await getOrCreateAccountAndServer(activity.actor) const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (account.url === activity.id) { if (actor.url === activity.id) {
return processDeleteAccount(account) if (actor.type === 'Person') {
if (!actor.Account) throw new Error('Actor ' + actor.url + ' is a person but we cannot find it in database.')
return processDeleteAccount(actor.Account)
} else if (actor.type === 'Group') {
if (!actor.VideoChannel) throw new Error('Actor ' + actor.url + ' is a group but we cannot find it in database.')
return processDeleteVideoChannel(actor.VideoChannel)
}
} }
{ {
let videoObject = await VideoModel.loadByUrlAndPopulateAccount(activity.id) let videoObject = await VideoModel.loadByUrlAndPopulateAccount(activity.id)
if (videoObject !== undefined) { if (videoObject !== undefined) {
return processDeleteVideo(account, videoObject) return processDeleteVideo(actor, videoObject)
}
}
{
let videoChannelObject = await VideoChannelModel.loadByUrl(activity.id)
if (videoChannelObject !== undefined) {
return processDeleteVideoChannel(account, videoChannelObject)
} }
} }
@ -38,21 +40,21 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processDeleteVideo (account: AccountModel, videoToDelete: VideoModel) { async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel) {
const options = { const options = {
arguments: [ account, videoToDelete ], arguments: [ actor, videoToDelete ],
errorMessage: 'Cannot remove the remote video with many retries.' errorMessage: 'Cannot remove the remote video with many retries.'
} }
await retryTransactionWrapper(deleteRemoteVideo, options) await retryTransactionWrapper(deleteRemoteVideo, options)
} }
async function deleteRemoteVideo (account: AccountModel, videoToDelete: VideoModel) { async function deleteRemoteVideo (actor: ActorModel, videoToDelete: VideoModel) {
logger.debug('Removing remote video "%s".', videoToDelete.uuid) logger.debug('Removing remote video "%s".', videoToDelete.uuid)
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
if (videoToDelete.VideoChannel.Account.id !== account.id) { if (videoToDelete.VideoChannel.Account.Actor.id !== actor.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url) throw new Error('Account ' + actor.url + ' does not own video channel ' + videoToDelete.VideoChannel.Actor.url)
} }
await videoToDelete.destroy({ transaction: t }) await videoToDelete.destroy({ transaction: t })
@ -61,29 +63,6 @@ async function deleteRemoteVideo (account: AccountModel, videoToDelete: VideoMod
logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
} }
async function processDeleteVideoChannel (account: AccountModel, videoChannelToRemove: VideoChannelModel) {
const options = {
arguments: [ account, videoChannelToRemove ],
errorMessage: 'Cannot remove the remote video channel with many retries.'
}
await retryTransactionWrapper(deleteRemoteVideoChannel, options)
}
async function deleteRemoteVideoChannel (account: AccountModel, videoChannelToRemove: VideoChannelModel) {
logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
await sequelizeTypescript.transaction(async t => {
if (videoChannelToRemove.Account.id !== account.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
}
await videoChannelToRemove.destroy({ transaction: t })
})
logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
}
async function processDeleteAccount (accountToRemove: AccountModel) { async function processDeleteAccount (accountToRemove: AccountModel) {
const options = { const options = {
arguments: [ accountToRemove ], arguments: [ accountToRemove ],
@ -94,11 +73,30 @@ async function processDeleteAccount (accountToRemove: AccountModel) {
} }
async function deleteRemoteAccount (accountToRemove: AccountModel) { async function deleteRemoteAccount (accountToRemove: AccountModel) {
logger.debug('Removing remote account "%s".', accountToRemove.uuid) logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid)
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
await accountToRemove.destroy({ transaction: t }) await accountToRemove.destroy({ transaction: t })
}) })
logger.info('Remote account with uuid %s removed.', accountToRemove.uuid) logger.info('Remote account with uuid %s removed.', accountToRemove.Actor.uuid)
}
async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) {
const options = {
arguments: [ videoChannelToRemove ],
errorMessage: 'Cannot remove the remote video channel with many retries.'
}
await retryTransactionWrapper(deleteRemoteVideoChannel, options)
}
async function deleteRemoteVideoChannel (videoChannelToRemove: VideoChannelModel) {
logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.uuid)
await sequelizeTypescript.transaction(async t => {
await videoChannelToRemove.destroy({ transaction: t })
})
logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.Actor.uuid)
} }

View File

@ -1,16 +1,16 @@
import { ActivityFollow } from '../../../../shared/models/activitypub' import { ActivityFollow } from '../../../../shared/models/activitypub'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
import { sendAccept } from '../send' import { sendAccept } from '../send'
async function processFollowActivity (activity: ActivityFollow) { async function processFollowActivity (activity: ActivityFollow) {
const activityObject = activity.object const activityObject = activity.object
const account = await getOrCreateAccountAndServer(activity.actor) const actor = await getOrCreateActorAndServerAndModel(activity.actor)
return processFollow(account, activityObject) return processFollow(actor, activityObject)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -21,46 +21,46 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function processFollow (account: AccountModel, targetAccountURL: string) { function processFollow (actor: ActorModel, targetActorURL: string) {
const options = { const options = {
arguments: [ account, targetAccountURL ], arguments: [ actor, targetActorURL ],
errorMessage: 'Cannot follow with many retries.' errorMessage: 'Cannot follow with many retries.'
} }
return retryTransactionWrapper(follow, options) return retryTransactionWrapper(follow, options)
} }
async function follow (account: AccountModel, targetAccountURL: string) { async function follow (actor: ActorModel, targetActorURL: string) {
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
const targetAccount = await AccountModel.loadByUrl(targetAccountURL, t) const targetActor = await ActorModel.loadByUrl(targetActorURL, t)
if (!targetAccount) throw new Error('Unknown account') if (!targetActor) throw new Error('Unknown actor')
if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
const [ accountFollow ] = await AccountFollowModel.findOrCreate({ const [ actorFollow ] = await ActorFollowModel.findOrCreate({
where: { where: {
accountId: account.id, actorId: actor.id,
targetAccountId: targetAccount.id targetActorId: targetActor.id
}, },
defaults: { defaults: {
accountId: account.id, actorId: actor.id,
targetAccountId: targetAccount.id, targetActorId: targetActor.id,
state: 'accepted' state: 'accepted'
}, },
transaction: t transaction: t
}) })
if (accountFollow.state !== 'accepted') { if (actorFollow.state !== 'accepted') {
accountFollow.state = 'accepted' actorFollow.state = 'accepted'
await accountFollow.save({ transaction: t }) await actorFollow.save({ transaction: t })
} }
accountFollow.AccountFollower = account actorFollow.ActorFollower = actor
accountFollow.AccountFollowing = targetAccount actorFollow.ActorFollowing = targetActor
// Target sends to account he accepted the follow request // Target sends to actor he accepted the follow request
return sendAccept(accountFollow, t) return sendAccept(actorFollow, t)
}) })
logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL) logger.info('Actor uuid %s is followed by actor %s.', actor.url, targetActorURL)
} }

View File

@ -1,16 +1,16 @@
import { ActivityLike } from '../../../../shared/models/activitypub' import { ActivityLike } from '../../../../shared/models/activitypub'
import { retryTransactionWrapper } from '../../../helpers' import { retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc' import { forwardActivity } from '../send/misc'
async function processLikeActivity (activity: ActivityLike) { async function processLikeActivity (activity: ActivityLike) {
const account = await getOrCreateAccountAndServer(activity.actor) const actor = await getOrCreateActorAndServerAndModel(activity.actor)
return processLikeVideo(account, activity) return processLikeVideo(actor, activity)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -21,18 +21,21 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processLikeVideo (byAccount: AccountModel, activity: ActivityLike) { async function processLikeVideo (actor: ActorModel, activity: ActivityLike) {
const options = { const options = {
arguments: [ byAccount, activity ], arguments: [ actor, 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: AccountModel, activity: ActivityLike) { function createVideoLike (byActor: ActorModel, activity: ActivityLike) {
const videoUrl = activity.object const videoUrl = activity.object
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadByUrlAndPopulateAccount(videoUrl) const video = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
@ -52,7 +55,7 @@ function createVideoLike (byAccount: AccountModel, activity: ActivityLike) {
if (video.isOwned() && created === true) { if (video.isOwned() && created === true) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byAccount ] const exceptions = [ byActor ]
await forwardActivity(activity, t, exceptions) await forwardActivity(activity, t, exceptions)
} }
}) })

View File

@ -3,8 +3,9 @@ import { DislikeObject } from '../../../../shared/models/activitypub/objects'
import { logger, retryTransactionWrapper } from '../../../helpers' import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { AccountFollowModel } from '../../../models/account/account-follow'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { forwardActivity } from '../send/misc' import { forwardActivity } from '../send/misc'
@ -32,21 +33,21 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function processUndoLike (actor: string, activity: ActivityUndo) { function processUndoLike (actorUrl: string, activity: ActivityUndo) {
const options = { const options = {
arguments: [ actor, activity ], arguments: [ actorUrl, 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, activity: ActivityUndo) { function undoLike (actorUrl: string, activity: ActivityUndo) {
const likeActivity = activity.object as ActivityLike const likeActivity = activity.object as ActivityLike
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const byAccount = await AccountModel.loadByUrl(actor, t) const byAccount = await AccountModel.loadByUrl(actorUrl, t)
if (!byAccount) throw new Error('Unknown account ' + actor) if (!byAccount) throw new Error('Unknown account ' + actorUrl)
const video = await VideoModel.loadByUrlAndPopulateAccount(likeActivity.object, t) const video = await VideoModel.loadByUrlAndPopulateAccount(likeActivity.object, t)
if (!video) throw new Error('Unknown video ' + likeActivity.actor) if (!video) throw new Error('Unknown video ' + likeActivity.actor)
@ -59,27 +60,27 @@ function undoLike (actor: string, activity: ActivityUndo) {
if (video.isOwned()) { if (video.isOwned()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byAccount ] const exceptions = [ byAccount.Actor ]
await forwardActivity(activity, t, exceptions) await forwardActivity(activity, t, exceptions)
} }
}) })
} }
function processUndoDislike (actor: string, activity: ActivityUndo) { function processUndoDislike (actorUrl: string, activity: ActivityUndo) {
const options = { const options = {
arguments: [ actor, activity ], arguments: [ actorUrl, 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, activity: ActivityUndo) { function undoDislike (actorUrl: string, activity: ActivityUndo) {
const dislike = activity.object.object as DislikeObject const dislike = activity.object.object as DislikeObject
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const byAccount = await AccountModel.loadByUrl(actor, t) const byAccount = await AccountModel.loadByUrl(actorUrl, t)
if (!byAccount) throw new Error('Unknown account ' + actor) if (!byAccount) throw new Error('Unknown account ' + actorUrl)
const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t) const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t)
if (!video) throw new Error('Unknown video ' + dislike.actor) if (!video) throw new Error('Unknown video ' + dislike.actor)
@ -92,30 +93,30 @@ function undoDislike (actor: string, activity: ActivityUndo) {
if (video.isOwned()) { if (video.isOwned()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byAccount ] const exceptions = [ byAccount.Actor ]
await forwardActivity(activity, t, exceptions) await forwardActivity(activity, t, exceptions)
} }
}) })
} }
function processUndoFollow (actor: string, followActivity: ActivityFollow) { function processUndoFollow (actorUrl: string, followActivity: ActivityFollow) {
const options = { const options = {
arguments: [ actor, followActivity ], arguments: [ actorUrl, followActivity ],
errorMessage: 'Cannot undo follow with many retries.' errorMessage: 'Cannot undo follow with many retries.'
} }
return retryTransactionWrapper(undoFollow, options) return retryTransactionWrapper(undoFollow, options)
} }
function undoFollow (actor: string, followActivity: ActivityFollow) { function undoFollow (actorUrl: string, followActivity: ActivityFollow) {
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const follower = await AccountModel.loadByUrl(actor, t) const follower = await ActorModel.loadByUrl(actorUrl, t)
const following = await AccountModel.loadByUrl(followActivity.object, t) const following = await ActorModel.loadByUrl(followActivity.object, t)
const accountFollow = await AccountFollowModel.loadByAccountAndTarget(follower.id, following.id, t) const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`) if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
await accountFollow.destroy({ transaction: t }) await actorFollow.destroy({ transaction: t })
return undefined return undefined
}) })

View File

@ -1,23 +1,19 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { VideoChannelObject, VideoTorrentObject } from '../../../../shared'
import { ActivityUpdate } from '../../../../shared/models/activitypub' import { ActivityUpdate } from '../../../../shared/models/activitypub'
import { logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers' import { logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { TagModel } from '../../../models/video/tag' import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoFileModel } from '../../../models/video/video-file' import { VideoFileModel } from '../../../models/video/video-file'
import { getOrCreateAccountAndServer } from '../account' import { getOrCreateActorAndServerAndModel } from '../actor'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processUpdateActivity (activity: ActivityUpdate) { async function processUpdateActivity (activity: ActivityUpdate) {
const account = await getOrCreateAccountAndServer(activity.actor) const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (activity.object.type === 'Video') { if (activity.object.type === 'Video') {
return processUpdateVideo(account, activity.object) return processUpdateVideo(actor, activity)
} else if (activity.object.type === 'VideoChannel') {
return processUpdateVideoChannel(account, activity.object)
} }
return return
@ -31,16 +27,18 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function processUpdateVideo (account: AccountModel, video: VideoTorrentObject) { function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
const options = { const options = {
arguments: [ account, video ], arguments: [ actor, activity ],
errorMessage: 'Cannot update the remote video with many retries' errorMessage: 'Cannot update the remote video with many retries'
} }
return retryTransactionWrapper(updateRemoteVideo, options) return retryTransactionWrapper(updateRemoteVideo, options)
} }
async function updateRemoteVideo (account: AccountModel, videoAttributesToUpdate: VideoTorrentObject) { async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
const videoAttributesToUpdate = activity.object
logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
let videoInstance: VideoModel let videoInstance: VideoModel
let videoFieldsSave: object let videoFieldsSave: object
@ -54,23 +52,23 @@ async function updateRemoteVideo (account: AccountModel, videoAttributesToUpdate
const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
if (videoInstance.VideoChannel.Account.id !== account.id) { const videoChannel = videoInstance.VideoChannel
throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url) if (videoChannel.Account.Actor.id !== actor.id) {
throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
} }
const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate) const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoAttributesToUpdate, activity.to, activity.cc)
videoInstance.set('name', videoData.name) videoInstance.set('name', videoData.name)
videoInstance.set('category', videoData.category) videoInstance.set('category', videoData.category)
videoInstance.set('licence', videoData.licence) videoInstance.set('licence', videoData.licence)
videoInstance.set('language', videoData.language) videoInstance.set('language', videoData.language)
videoInstance.set('nsfw', videoData.nsfw) videoInstance.set('nsfw', videoData.nsfw)
videoInstance.set('privacy', videoData.privacy)
videoInstance.set('description', videoData.description) videoInstance.set('description', videoData.description)
videoInstance.set('duration', videoData.duration) videoInstance.set('duration', videoData.duration)
videoInstance.set('createdAt', videoData.createdAt) videoInstance.set('createdAt', videoData.createdAt)
videoInstance.set('updatedAt', videoData.updatedAt) videoInstance.set('updatedAt', videoData.updatedAt)
videoInstance.set('views', videoData.views) videoInstance.set('views', videoData.views)
// videoInstance.set('likes', videoData.likes)
// videoInstance.set('dislikes', videoData.dislikes)
await videoInstance.save(sequelizeOptions) await videoInstance.save(sequelizeOptions)
@ -101,36 +99,3 @@ async function updateRemoteVideo (account: AccountModel, videoAttributesToUpdate
throw err throw err
} }
} }
async function processUpdateVideoChannel (account: AccountModel, videoChannel: VideoChannelObject) {
const options = {
arguments: [ account, videoChannel ],
errorMessage: 'Cannot update the remote video channel with many retries.'
}
await retryTransactionWrapper(updateRemoteVideoChannel, options)
}
async function updateRemoteVideoChannel (account: AccountModel, videoChannel: VideoChannelObject) {
logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
const videoChannelInstance = await VideoChannelModel.loadByUrl(videoChannel.id)
if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
if (videoChannelInstance.Account.id !== account.id) {
throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
}
videoChannelInstance.set('name', videoChannel.name)
videoChannelInstance.set('description', videoChannel.content)
videoChannelInstance.set('createdAt', videoChannel.published)
videoChannelInstance.set('updatedAt', videoChannel.updated)
await videoChannelInstance.save(sequelizeOptions)
})
logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
}

View File

@ -1,8 +1,7 @@
import { Activity, ActivityType } from '../../../../shared/models/activitypub' import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { processAcceptActivity } from './process-accept' import { processAcceptActivity } from './process-accept'
import { processAddActivity } from './process-add'
import { processAnnounceActivity } from './process-announce' import { processAnnounceActivity } from './process-announce'
import { processCreateActivity } from './process-create' import { processCreateActivity } from './process-create'
import { processDeleteActivity } from './process-delete' import { processDeleteActivity } from './process-delete'
@ -11,9 +10,8 @@ import { processLikeActivity } from './process-like'
import { processUndoActivity } from './process-undo' import { processUndoActivity } from './process-undo'
import { processUpdateActivity } from './process-update' import { processUpdateActivity } from './process-update'
const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountModel) => Promise<any> } = { const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxActor?: ActorModel) => Promise<any> } = {
Create: processCreateActivity, Create: processCreateActivity,
Add: processAddActivity,
Update: processUpdateActivity, Update: processUpdateActivity,
Delete: processDeleteActivity, Delete: processDeleteActivity,
Follow: processFollowActivity, Follow: processFollowActivity,
@ -23,11 +21,11 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
Like: processLikeActivity Like: processLikeActivity
} }
async function processActivities (activities: Activity[], signatureAccount?: AccountModel, inboxAccount?: AccountModel) { async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) {
for (const activity of activities) { for (const activity of activities) {
// When we fetch remote data, we don't have signature // When we fetch remote data, we don't have signature
if (signatureAccount && activity.actor !== signatureAccount.url) { if (signatureActor && activity.actor !== signatureActor.url) {
logger.warn('Signature mismatch between %s and %s.', activity.actor, signatureAccount.url) logger.warn('Signature mismatch between %s and %s.', activity.actor, signatureActor.url)
continue continue
} }
@ -38,7 +36,7 @@ async function processActivities (activities: Activity[], signatureAccount?: Acc
} }
try { try {
await activityProcessor(activity, inboxAccount) await activityProcessor(activity, inboxActor)
} catch (err) { } catch (err) {
logger.warn('Cannot process activity %s.', activity.type, err) logger.warn('Cannot process activity %s.', activity.type, err)
} }

View File

@ -1,5 +1,4 @@
export * from './send-accept' export * from './send-accept'
export * from './send-add'
export * from './send-announce' export * from './send-announce'
export * from './send-create' export * from './send-create'
export * from './send-delete' export * from './send-delete'

View File

@ -2,18 +2,16 @@ import { Transaction } from 'sequelize'
import { Activity } from '../../../../shared/models/activitypub' import { Activity } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers' import { ACTIVITY_PUB } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler' import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler'
async function forwardActivity ( async function forwardActivity (
activity: Activity, activity: Activity,
t: Transaction, t: Transaction,
followersException: AccountModel[] = [] followersException: ActorModel[] = []
) { ) {
const to = activity.to || [] const to = activity.to || []
const cc = activity.cc || [] const cc = activity.cc || []
@ -25,11 +23,11 @@ async function forwardActivity (
} }
} }
const toAccountFollowers = await AccountModel.listByFollowersUrls(followersUrls, t) const toActorFollowers = await ActorModel.listByFollowersUrls(followersUrls, t)
const uris = await computeFollowerUris(toAccountFollowers, followersException, t) const uris = await computeFollowerUris(toActorFollowers, followersException, t)
if (uris.length === 0) { if (uris.length === 0) {
logger.info('0 followers for %s, no forwarding.', toAccountFollowers.map(a => a.id).join(', ')) logger.info('0 followers for %s, no forwarding.', toActorFollowers.map(a => a.id).join(', '))
return undefined return undefined
} }
@ -45,14 +43,14 @@ async function forwardActivity (
async function broadcastToFollowers ( async function broadcastToFollowers (
data: any, data: any,
byAccount: AccountModel, byActor: ActorModel,
toAccountFollowers: AccountModel[], toActorFollowers: ActorModel[],
t: Transaction, t: Transaction,
followersException: AccountModel[] = [] followersException: ActorModel[] = []
) { ) {
const uris = await computeFollowerUris(toAccountFollowers, followersException, t) const uris = await computeFollowerUris(toActorFollowers, followersException, t)
if (uris.length === 0) { if (uris.length === 0) {
logger.info('0 followers for %s, no broadcasting.', toAccountFollowers.map(a => a.id).join(', ')) logger.info('0 followers for %s, no broadcasting.', toActorFollowers.map(a => a.id).join(', '))
return undefined return undefined
} }
@ -60,62 +58,48 @@ async function broadcastToFollowers (
const jobPayload: ActivityPubHttpPayload = { const jobPayload: ActivityPubHttpPayload = {
uris, uris,
signatureAccountId: byAccount.id, signatureActorId: byActor.id,
body: data body: data
} }
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload)
} }
async function unicastTo (data: any, byAccount: AccountModel, toAccountUrl: string, t: Transaction) { async function unicastTo (data: any, byActor: ActorModel, toActorUrl: string, t: Transaction) {
logger.debug('Creating unicast job.', { uri: toAccountUrl }) logger.debug('Creating unicast job.', { uri: toActorUrl })
const jobPayload: ActivityPubHttpPayload = { const jobPayload: ActivityPubHttpPayload = {
uris: [ toAccountUrl ], uris: [ toActorUrl ],
signatureAccountId: byAccount.id, signatureActorId: byActor.id,
body: data body: data
} }
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
} }
function getOriginVideoAudience (video: VideoModel, accountsInvolvedInVideo: AccountModel[]) { function getOriginVideoAudience (video: VideoModel, actorsInvolvedInVideo: ActorModel[]) {
return { return {
to: [ video.VideoChannel.Account.url ], to: [ video.VideoChannel.Account.Actor.url ],
cc: accountsInvolvedInVideo.map(a => a.followersUrl) cc: actorsInvolvedInVideo.map(a => a.followersUrl)
} }
} }
function getOriginVideoChannelAudience (videoChannel: VideoChannelModel, accountsInvolved: AccountModel[]) { function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
return { return {
to: [ videoChannel.Account.url ], to: actorsInvolvedInObject.map(a => a.followersUrl),
cc: accountsInvolved.map(a => a.followersUrl)
}
}
function getObjectFollowersAudience (accountsInvolvedInObject: AccountModel[]) {
return {
to: accountsInvolvedInObject.map(a => a.followersUrl),
cc: [] cc: []
} }
} }
async function getAccountsInvolvedInVideo (video: VideoModel, t: Transaction) { async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
const accountsToForwardView = await VideoShareModel.loadAccountsByShare(video.id, t) const actorsToForwardView = await VideoShareModel.loadActorsByShare(video.id, t)
accountsToForwardView.push(video.VideoChannel.Account) actorsToForwardView.push(video.VideoChannel.Account.Actor)
return accountsToForwardView return actorsToForwardView
} }
async function getAccountsInvolvedInVideoChannel (videoChannel: VideoChannelModel, t: Transaction) { async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
const accountsToForwardView = await VideoChannelShareModel.loadAccountsByShare(videoChannel.id, t) const followerInboxUrls = await actorSender.getFollowerSharedInboxUrls(t)
accountsToForwardView.push(videoChannel.Account)
return accountsToForwardView
}
async function getAudience (accountSender: AccountModel, t: Transaction, isPublic = true) {
const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls(t)
// Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47 // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47
let to = [] let to = []
@ -132,10 +116,10 @@ async function getAudience (accountSender: AccountModel, t: Transaction, isPubli
return { to, cc } return { to, cc }
} }
async function computeFollowerUris (toAccountFollower: AccountModel[], followersException: AccountModel[], t: Transaction) { async function computeFollowerUris (toActorFollower: ActorModel[], followersException: ActorModel[], t: Transaction) {
const toAccountFollowerIds = toAccountFollower.map(a => a.id) const toActorFollowerIds = toActorFollower.map(a => a.id)
const result = await AccountFollowModel.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds, t) const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl)
return result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1) return result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1)
} }
@ -144,12 +128,10 @@ async function computeFollowerUris (toAccountFollower: AccountModel[], followers
export { export {
broadcastToFollowers, broadcastToFollowers,
getOriginVideoChannelAudience,
unicastTo, unicastTo,
getAudience, getAudience,
getOriginVideoAudience, getOriginVideoAudience,
getAccountsInvolvedInVideo, getActorsInvolvedInVideo,
getAccountsInvolvedInVideoChannel,
getObjectFollowersAudience, getObjectFollowersAudience,
forwardActivity forwardActivity
} }

View File

@ -1,15 +1,15 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAccept } from '../../../../shared/models/activitypub' import { ActivityAccept } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getAccountFollowAcceptActivityPubUrl } from '../url' import { getActorFollowAcceptActivityPubUrl } from '../url'
import { unicastTo } from './misc' import { unicastTo } from './misc'
async function sendAccept (accountFollow: AccountFollowModel, t: Transaction) { async function sendAccept (actorFollow: ActorFollowModel, t: Transaction) {
const follower = accountFollow.AccountFollower const follower = actorFollow.ActorFollower
const me = accountFollow.AccountFollowing const me = actorFollow.ActorFollowing
const url = getAccountFollowAcceptActivityPubUrl(accountFollow) const url = getActorFollowAcceptActivityPubUrl(actorFollow)
const data = acceptActivityData(url, me) const data = acceptActivityData(url, me)
return unicastTo(data, me, follower.inboxUrl, t) return unicastTo(data, me, follower.inboxUrl, t)
@ -23,12 +23,10 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function acceptActivityData (url: string, byAccount: AccountModel) { function acceptActivityData (url: string, byActor: ActorModel): ActivityAccept {
const activity: ActivityAccept = { return {
type: 'Accept', type: 'Accept',
id: url, id: url,
actor: byAccount.url actor: byActor.url
} }
return activity
} }

View File

@ -1,45 +0,0 @@
import { Transaction } from 'sequelize'
import { ActivityAdd } from '../../../../shared/models/activitypub'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video'
import { broadcastToFollowers, getAudience } from './misc'
async function sendAddVideo (video: VideoModel, t: Transaction) {
const byAccount = video.VideoChannel.Account
const videoObject = video.toActivityPubObject()
const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject, t)
return broadcastToFollowers(data, byAccount, [ byAccount ], t)
}
async function addActivityData (
url: string,
byAccount: AccountModel,
video: VideoModel,
target: string,
object: any,
t: Transaction
): Promise<ActivityAdd> {
const videoPublic = video.privacy === VideoPrivacy.PUBLIC
const { to, cc } = await getAudience(byAccount, t, videoPublic)
return {
type: 'Add',
id: url,
actor: byAccount.url,
to,
cc,
object,
target
}
}
// ---------------------------------------------------------------------------
export {
addActivityData,
sendAddVideo
}

View File

@ -1,88 +1,59 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAdd } from '../../../../shared/index'
import { ActivityAnnounce, ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub' import { ActivityAnnounce, ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { VideoPrivacy } from '../../../../shared/models/videos'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { getAnnounceActivityPubUrl } from '../url' import { getAnnounceActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsInvolvedInVideo, getActorsInvolvedInVideo,
getAccountsInvolvedInVideoChannel,
getAudience, getAudience,
getObjectFollowersAudience, getObjectFollowersAudience,
getOriginVideoAudience, getOriginVideoAudience,
getOriginVideoChannelAudience,
unicastTo unicastTo
} from './misc' } from './misc'
import { addActivityData } from './send-add'
import { createActivityData } from './send-create' import { createActivityData } from './send-create'
async function buildVideoAnnounceToFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function buildVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(video.url, byAccount) const url = getAnnounceActivityPubUrl(video.url, byActor)
const videoObject = video.toActivityPubObject()
const videoChannel = video.VideoChannel const announcedAudience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject(), t) const announcedActivity = await createActivityData(url, video.VideoChannel.Account.Actor, videoObject, t, announcedAudience)
const accountsToForwardView = await getAccountsInvolvedInVideo(video, t) const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsToForwardView) const audience = getObjectFollowersAudience(accountsToForwardView)
return announceActivityData(url, byAccount, announcedActivity, t, audience) return announceActivityData(url, byActor, announcedActivity, t, audience)
} }
async function sendVideoAnnounceToFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const data = await buildVideoAnnounceToFollowers(byAccount, video, t) const data = await buildVideoAnnounceToFollowers(byActor, video, t)
return broadcastToFollowers(data, byAccount, [ byAccount ], t) return broadcastToFollowers(data, byActor, [ byActor ], t)
} }
async function sendVideoAnnounceToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendVideoAnnounceToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(video.url, byAccount) const url = getAnnounceActivityPubUrl(video.url, byActor)
const videoChannel = video.VideoChannel const videoObject = video.toActivityPubObject()
const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject(), t) const announcedActivity = await createActivityData(url, video.VideoChannel.Account.Actor, videoObject, t)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const data = await createActivityData(url, byAccount, announcedActivity, t, audience) const data = await createActivityData(url, byActor, announcedActivity, t, audience)
return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
}
async function buildVideoChannelAnnounceToFollowers (byAccount: AccountModel, videoChannel: VideoChannelModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount)
const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), t)
const accountsToForwardView = await getAccountsInvolvedInVideoChannel(videoChannel, t)
const audience = getObjectFollowersAudience(accountsToForwardView)
return announceActivityData(url, byAccount, announcedActivity, t, audience)
}
async function sendVideoChannelAnnounceToFollowers (byAccount: AccountModel, videoChannel: VideoChannelModel, t: Transaction) {
const data = await buildVideoChannelAnnounceToFollowers(byAccount, videoChannel, t)
return broadcastToFollowers(data, byAccount, [ byAccount ], t)
}
async function sendVideoChannelAnnounceToOrigin (byAccount: AccountModel, videoChannel: VideoChannelModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount)
const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), t)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideoChannel(videoChannel, t)
const audience = getOriginVideoChannelAudience(videoChannel, accountsInvolvedInVideo)
const data = await createActivityData(url, byAccount, announcedActivity, t, audience)
return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t)
} }
async function announceActivityData ( async function announceActivityData (
url: string, url: string,
byAccount: AccountModel, byActor: ActorModel,
object: ActivityCreate | ActivityAdd, object: ActivityCreate,
t: Transaction, t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityAnnounce> { ): Promise<ActivityAnnounce> {
if (!audience) { if (!audience) {
audience = await getAudience(byAccount, t) audience = await getAudience(byActor, t)
} }
return { return {
@ -90,7 +61,7 @@ async function announceActivityData (
to: audience.to, to: audience.to,
cc: audience.cc, cc: audience.cc,
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
object object
} }
} }
@ -99,10 +70,7 @@ async function announceActivityData (
export { export {
sendVideoAnnounceToFollowers, sendVideoAnnounceToFollowers,
sendVideoChannelAnnounceToFollowers,
sendVideoAnnounceToOrigin, sendVideoAnnounceToOrigin,
sendVideoChannelAnnounceToOrigin,
announceActivityData, announceActivityData,
buildVideoAnnounceToFollowers, buildVideoAnnounceToFollowers
buildVideoChannelAnnounceToFollowers
} }

View File

@ -1,111 +1,112 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub' import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
import { getServerAccount } from '../../../helpers' import { VideoPrivacy } from '../../../../shared/models/videos'
import { AccountModel } from '../../../models/account/account' import { getServerActor } from '../../../helpers'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoAbuseModel } from '../../../models/video/video-abuse' import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
getObjectFollowersAudience, getObjectFollowersAudience,
getOriginVideoAudience, getOriginVideoAudience,
unicastTo unicastTo
} from './misc' } from './misc'
async function sendCreateVideoChannel (videoChannel: VideoChannelModel, t: Transaction) { async function sendCreateVideo (video: VideoModel, t: Transaction) {
const byAccount = videoChannel.Account const byActor = video.VideoChannel.Account.Actor
const videoChannelObject = videoChannel.toActivityPubObject() const videoObject = video.toActivityPubObject()
const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject, t) const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
const data = await createActivityData(video.url, byActor, videoObject, t, audience)
return broadcastToFollowers(data, byAccount, [ byAccount ], t) return broadcastToFollowers(data, byActor, [ byActor ], t)
} }
async function sendVideoAbuse (byAccount: AccountModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) { async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) {
const url = getVideoAbuseActivityPubUrl(videoAbuse) const url = getVideoAbuseActivityPubUrl(videoAbuse)
const audience = { to: [ video.VideoChannel.Account.url ], cc: [] } const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject(), t, audience) const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendCreateViewToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoViewActivityPubUrl(byAccount, video) const url = getVideoViewActivityPubUrl(byActor, video)
const viewActivity = createViewActivityData(byAccount, video) const viewActivity = createViewActivityData(byActor, video)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const data = await createActivityData(url, byAccount, viewActivity, t, audience) const data = await createActivityData(url, byActor, viewActivity, t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendCreateViewToVideoFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendCreateViewToVideoFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoViewActivityPubUrl(byAccount, video) const url = getVideoViewActivityPubUrl(byActor, video)
const viewActivity = createViewActivityData(byAccount, video) const viewActivity = createViewActivityData(byActor, video)
const accountsToForwardView = await getAccountsInvolvedInVideo(video, t) const actorsToForwardView = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsToForwardView) const audience = getObjectFollowersAudience(actorsToForwardView)
const data = await createActivityData(url, byAccount, viewActivity, t, audience) const data = await createActivityData(url, byActor, viewActivity, t, audience)
// Use the server account to send the view, because it could be an unregistered account // Use the server actor to send the view
const serverAccount = await getServerAccount() const serverActor = await getServerActor()
const followersException = [ byAccount ] const followersException = [ byActor ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, serverActor, actorsToForwardView, t, followersException)
} }
async function sendCreateDislikeToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendCreateDislikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoDislikeActivityPubUrl(byAccount, video) const url = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const data = await createActivityData(url, byAccount, dislikeActivity, t, audience) const data = await createActivityData(url, byActor, dislikeActivity, t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendCreateDislikeToVideoFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendCreateDislikeToVideoFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoDislikeActivityPubUrl(byAccount, video) const url = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const accountsToForwardView = await getAccountsInvolvedInVideo(video, t) const actorsToForwardView = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsToForwardView) const audience = getObjectFollowersAudience(actorsToForwardView)
const data = await createActivityData(url, byAccount, dislikeActivity, t, audience) const data = await createActivityData(url, byActor, dislikeActivity, t, audience)
const followersException = [ byAccount ] const followersException = [ byActor ]
return broadcastToFollowers(data, byAccount, accountsToForwardView, t, followersException) return broadcastToFollowers(data, byActor, actorsToForwardView, t, followersException)
} }
async function createActivityData ( async function createActivityData (
url: string, url: string,
byAccount: AccountModel, byActor: ActorModel,
object: any, object: any,
t: Transaction, t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityCreate> { ): Promise<ActivityCreate> {
if (!audience) { if (!audience) {
audience = await getAudience(byAccount, t) audience = await getAudience(byActor, t)
} }
return { return {
type: 'Create', type: 'Create',
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
to: audience.to, to: audience.to,
cc: audience.cc, cc: audience.cc,
object object
} }
} }
function createDislikeActivityData (byAccount: AccountModel, video: VideoModel) { function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
return { return {
type: 'Dislike', type: 'Dislike',
actor: byAccount.url, actor: byActor.url,
object: video.url object: video.url
} }
} }
@ -113,7 +114,7 @@ function createDislikeActivityData (byAccount: AccountModel, video: VideoModel)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
sendCreateVideoChannel, sendCreateVideo,
sendVideoAbuse, sendVideoAbuse,
createActivityData, createActivityData,
sendCreateViewToOrigin, sendCreateViewToOrigin,
@ -125,10 +126,10 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function createViewActivityData (byAccount: AccountModel, video: VideoModel) { function createViewActivityData (byActor: ActorModel, video: VideoModel) {
return { return {
type: 'View', type: 'View',
actor: byAccount.url, actor: byActor.url,
object: video.url object: video.url
} }
} }

View File

@ -1,54 +1,40 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityDelete } from '../../../../shared/models/activitypub' import { ActivityDelete } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { broadcastToFollowers } from './misc' import { broadcastToFollowers } from './misc'
async function sendDeleteVideoChannel (videoChannel: VideoChannelModel, t: Transaction) {
const byAccount = videoChannel.Account
const data = deleteActivityData(videoChannel.url, byAccount)
const accountsInvolved = await VideoChannelShareModel.loadAccountsByShare(videoChannel.id, t)
accountsInvolved.push(byAccount)
return broadcastToFollowers(data, byAccount, accountsInvolved, t)
}
async function sendDeleteVideo (video: VideoModel, t: Transaction) { async function sendDeleteVideo (video: VideoModel, t: Transaction) {
const byAccount = video.VideoChannel.Account const byActor = video.VideoChannel.Account.Actor
const data = deleteActivityData(video.url, byAccount) const data = deleteActivityData(video.url, byActor)
const accountsInvolved = await VideoShareModel.loadAccountsByShare(video.id, t) const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
accountsInvolved.push(byAccount) actorsInvolved.push(byActor)
return broadcastToFollowers(data, byAccount, accountsInvolved, t) return broadcastToFollowers(data, byActor, actorsInvolved, t)
} }
async function sendDeleteAccount (account: AccountModel, t: Transaction) { async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
const data = deleteActivityData(account.url, account) const data = deleteActivityData(byActor.url, byActor)
return broadcastToFollowers(data, account, [ account ], t) return broadcastToFollowers(data, byActor, [ byActor ], t)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
sendDeleteVideoChannel,
sendDeleteVideo, sendDeleteVideo,
sendDeleteAccount sendDeleteActor
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function deleteActivityData (url: string, byAccount: AccountModel): ActivityDelete { function deleteActivityData (url: string, byActor: ActorModel): ActivityDelete {
return { return {
type: 'Delete', type: 'Delete',
id: url, id: url,
actor: byAccount.url actor: byActor.url
} }
} }

View File

@ -1,26 +1,26 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityFollow } from '../../../../shared/models/activitypub' import { ActivityFollow } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getAccountFollowActivityPubUrl } from '../url' import { getActorFollowActivityPubUrl } from '../url'
import { unicastTo } from './misc' import { unicastTo } from './misc'
function sendFollow (accountFollow: AccountFollowModel, t: Transaction) { function sendFollow (actorFollow: ActorFollowModel, t: Transaction) {
const me = accountFollow.AccountFollower const me = actorFollow.ActorFollower
const following = accountFollow.AccountFollowing const following = actorFollow.ActorFollowing
const url = getAccountFollowActivityPubUrl(accountFollow) const url = getActorFollowActivityPubUrl(actorFollow)
const data = followActivityData(url, me, following) const data = followActivityData(url, me, following)
return unicastTo(data, me, following.inboxUrl, t) return unicastTo(data, me, following.inboxUrl, t)
} }
function followActivityData (url: string, byAccount: AccountModel, targetAccount: AccountModel): ActivityFollow { function followActivityData (url: string, byActor: ActorModel, targetActor: ActorModel): ActivityFollow {
return { return {
type: 'Follow', type: 'Follow',
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
object: targetAccount.url object: targetActor.url
} }
} }

View File

@ -1,53 +1,53 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub' import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { getVideoLikeActivityPubUrl } from '../url' import { getVideoLikeActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
getOriginVideoAudience,
getObjectFollowersAudience, getObjectFollowersAudience,
getOriginVideoAudience,
unicastTo unicastTo
} from './misc' } from './misc'
async function sendLikeToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendLikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video) const url = getVideoLikeActivityPubUrl(byActor, video)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const accountsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
const data = await likeActivityData(url, byAccount, video, t, audience) const data = await likeActivityData(url, byActor, video, t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendLikeToVideoFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendLikeToVideoFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video) const url = getVideoLikeActivityPubUrl(byActor, video)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const accountsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsInvolvedInVideo) const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
const data = await likeActivityData(url, byAccount, video, t, audience) const data = await likeActivityData(url, byActor, video, t, audience)
const followersException = [ byAccount ] const followersException = [ byActor ]
return broadcastToFollowers(data, byAccount, accountsInvolvedInVideo, t, followersException) return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
} }
async function likeActivityData ( async function likeActivityData (
url: string, url: string,
byAccount: AccountModel, byActor: ActorModel,
video: VideoModel, video: VideoModel,
t: Transaction, t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityLike> { ): Promise<ActivityLike> {
if (!audience) { if (!audience) {
audience = await getAudience(byAccount, t) audience = await getAudience(byActor, t)
} }
return { return {
type: 'Like', type: 'Like',
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
to: audience.to, to: audience.to,
cc: audience.cc, cc: audience.cc,
object: video.url object: video.url

View File

@ -6,13 +6,13 @@ import {
ActivityLike, ActivityLike,
ActivityUndo ActivityUndo
} from '../../../../shared/models/activitypub' } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { AccountFollowModel } from '../../../models/account/account-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
import { import {
broadcastToFollowers, broadcastToFollowers,
getAccountsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
getObjectFollowersAudience, getObjectFollowersAudience,
getOriginVideoAudience, getOriginVideoAudience,
@ -22,11 +22,11 @@ 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'
async function sendUndoFollow (accountFollow: AccountFollowModel, t: Transaction) { async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
const me = accountFollow.AccountFollower const me = actorFollow.ActorFollower
const following = accountFollow.AccountFollowing const following = actorFollow.ActorFollowing
const followUrl = getAccountFollowActivityPubUrl(accountFollow) const followUrl = getActorFollowActivityPubUrl(actorFollow)
const undoUrl = getUndoActivityPubUrl(followUrl) const undoUrl = getUndoActivityPubUrl(followUrl)
const object = followActivityData(followUrl, me, following) const object = followActivityData(followUrl, me, following)
@ -35,58 +35,58 @@ async function sendUndoFollow (accountFollow: AccountFollowModel, t: Transaction
return unicastTo(data, me, following.inboxUrl, t) return unicastTo(data, me, following.inboxUrl, t)
} }
async function sendUndoLikeToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendUndoLikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const likeUrl = getVideoLikeActivityPubUrl(byAccount, video) const likeUrl = getVideoLikeActivityPubUrl(byActor, video)
const undoUrl = getUndoActivityPubUrl(likeUrl) const undoUrl = getUndoActivityPubUrl(likeUrl)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const object = await likeActivityData(likeUrl, byAccount, video, t) const object = await likeActivityData(likeUrl, byActor, video, t)
const data = await undoActivityData(undoUrl, byAccount, object, t, audience) const data = await undoActivityData(undoUrl, byActor, object, t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendUndoLikeToVideoFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendUndoLikeToVideoFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const likeUrl = getVideoLikeActivityPubUrl(byAccount, video) const likeUrl = getVideoLikeActivityPubUrl(byActor, video)
const undoUrl = getUndoActivityPubUrl(likeUrl) const undoUrl = getUndoActivityPubUrl(likeUrl)
const toAccountsFollowers = await getAccountsInvolvedInVideo(video, t) const toActorsFollowers = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(toAccountsFollowers) const audience = getObjectFollowersAudience(toActorsFollowers)
const object = await likeActivityData(likeUrl, byAccount, video, t) const object = await likeActivityData(likeUrl, byActor, video, t)
const data = await undoActivityData(undoUrl, byAccount, object, t, audience) const data = await undoActivityData(undoUrl, byActor, object, t, audience)
const followersException = [ byAccount ] const followersException = [ byActor ]
return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException) return broadcastToFollowers(data, byActor, toActorsFollowers, t, followersException)
} }
async function sendUndoDislikeToOrigin (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendUndoDislikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video) const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
const undoUrl = getUndoActivityPubUrl(dislikeUrl) const undoUrl = getUndoActivityPubUrl(dislikeUrl)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const object = await createActivityData(undoUrl, byAccount, dislikeActivity, t) const object = await createActivityData(undoUrl, byActor, dislikeActivity, t)
const data = await undoActivityData(undoUrl, byAccount, object, t, audience) const data = await undoActivityData(undoUrl, byActor, object, t, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
} }
async function sendUndoDislikeToVideoFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) { async function sendUndoDislikeToVideoFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video) const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
const undoUrl = getUndoActivityPubUrl(dislikeUrl) const undoUrl = getUndoActivityPubUrl(dislikeUrl)
const dislikeActivity = createDislikeActivityData(byAccount, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const object = await createActivityData(undoUrl, byAccount, dislikeActivity, t) const object = await createActivityData(undoUrl, byActor, dislikeActivity, t)
const data = await undoActivityData(undoUrl, byAccount, object, t) const data = await undoActivityData(undoUrl, byActor, object, t)
const toAccountsFollowers = await getAccountsInvolvedInVideo(video, t) const toActorsFollowers = await getActorsInvolvedInVideo(video, t)
const followersException = [ byAccount ] const followersException = [ byActor ]
return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException) return broadcastToFollowers(data, byActor, toActorsFollowers, t, followersException)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -103,19 +103,19 @@ export {
async function undoActivityData ( async function undoActivityData (
url: string, url: string,
byAccount: AccountModel, byActor: ActorModel,
object: ActivityFollow | ActivityLike | ActivityCreate, object: ActivityFollow | ActivityLike | ActivityCreate,
t: Transaction, t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityUndo> { ): Promise<ActivityUndo> {
if (!audience) { if (!audience) {
audience = await getAudience(byAccount, t) audience = await getAudience(byActor, t)
} }
return { return {
type: 'Undo', type: 'Undo',
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
to: audience.to, to: audience.to,
cc: audience.cc, cc: audience.cc,
object object

View File

@ -1,56 +1,52 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityUpdate } from '../../../../shared/models/activitypub' import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account' import { VideoPrivacy } from '../../../../shared/models/videos'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { getUpdateActivityPubUrl } from '../url' import { getUpdateActivityPubUrl } from '../url'
import { broadcastToFollowers, getAudience } from './misc' import { broadcastToFollowers, getAudience } from './misc'
async function sendUpdateVideoChannel (videoChannel: VideoChannelModel, t: Transaction) {
const byAccount = videoChannel.Account
const url = getUpdateActivityPubUrl(videoChannel.url, videoChannel.updatedAt.toISOString())
const videoChannelObject = videoChannel.toActivityPubObject()
const data = await updateActivityData(url, byAccount, videoChannelObject, t)
const accountsInvolved = await VideoChannelShareModel.loadAccountsByShare(videoChannel.id, t)
accountsInvolved.push(byAccount)
return broadcastToFollowers(data, byAccount, accountsInvolved, t)
}
async function sendUpdateVideo (video: VideoModel, t: Transaction) { async function sendUpdateVideo (video: VideoModel, t: Transaction) {
const byAccount = video.VideoChannel.Account const byActor = video.VideoChannel.Account.Actor
const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
const videoObject = video.toActivityPubObject() const videoObject = video.toActivityPubObject()
const data = await updateActivityData(url, byAccount, videoObject, t) const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
const accountsInvolved = await VideoShareModel.loadAccountsByShare(video.id, t) const data = await updateActivityData(url, byActor, videoObject, t, audience)
accountsInvolved.push(byAccount)
return broadcastToFollowers(data, byAccount, accountsInvolved, t) const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
actorsInvolved.push(byActor)
return broadcastToFollowers(data, byActor, actorsInvolved, t)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
sendUpdateVideoChannel,
sendUpdateVideo sendUpdateVideo
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function updateActivityData (url: string, byAccount: AccountModel, object: any, t: Transaction): Promise<ActivityUpdate> { async function updateActivityData (
const { to, cc } = await getAudience(byAccount, t) url: string,
byActor: ActorModel,
object: any,
t: Transaction,
audience?: ActivityAudience
): Promise<ActivityUpdate> {
if (!audience) {
audience = await getAudience(byActor, t)
}
return { return {
type: 'Update', type: 'Update',
id: url, id: url,
actor: byAccount.url, actor: byActor.url,
to, to: audience.to,
cc, cc: audience.cc,
object object
} }
} }

View File

@ -1,34 +1,20 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { getServerAccount } from '../../helpers' import { getServerActor } from '../../helpers'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoChannelShareModel } from '../../models/video/video-channel-share'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { sendVideoAnnounceToFollowers, sendVideoChannelAnnounceToFollowers } from './send' import { sendVideoAnnounceToFollowers } from './send'
async function shareVideoChannelByServer (videoChannel: VideoChannelModel, t: Transaction) {
const serverAccount = await getServerAccount()
await VideoChannelShareModel.create({
accountId: serverAccount.id,
videoChannelId: videoChannel.id
}, { transaction: t })
return sendVideoChannelAnnounceToFollowers(serverAccount, videoChannel, t)
}
async function shareVideoByServer (video: VideoModel, t: Transaction) { async function shareVideoByServer (video: VideoModel, t: Transaction) {
const serverAccount = await getServerAccount() const serverActor = await getServerActor()
await VideoShareModel.create({ await VideoShareModel.create({
accountId: serverAccount.id, actorId: serverActor.id,
videoId: video.id videoId: video.id
}, { transaction: t }) }, { transaction: t })
return sendVideoAnnounceToFollowers(serverAccount, video, t) return sendVideoAnnounceToFollowers(serverActor, video, t)
} }
export { export {
shareVideoChannelByServer,
shareVideoByServer shareVideoByServer
} }

View File

@ -1,16 +1,19 @@
import { CONFIG } from '../../initializers' import { CONFIG } from '../../initializers'
import { AccountModel } from '../../models/account/account' import { ActorModel } from '../../models/activitypub/actor'
import { AccountFollowModel } from '../../models/account/account-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoAbuseModel } from '../../models/video/video-abuse' import { VideoAbuseModel } from '../../models/video/video-abuse'
import { VideoChannelModel } from '../../models/video/video-channel'
function getVideoActivityPubUrl (video: VideoModel) { function getVideoActivityPubUrl (video: VideoModel) {
return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
} }
function getVideoChannelActivityPubUrl (videoChannel: VideoChannelModel) { function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID
}
function getApplicationActivityPubUrl () {
return CONFIG.WEBSERVER.URL + '/application/peertube'
} }
function getAccountActivityPubUrl (accountName: string) { function getAccountActivityPubUrl (accountName: string) {
@ -21,34 +24,34 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
} }
function getVideoViewActivityPubUrl (byAccount: AccountModel, video: VideoModel) { function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/views/' + byAccount.uuid + '/' + new Date().toISOString() return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString()
} }
function getVideoLikeActivityPubUrl (byAccount: AccountModel, video: VideoModel) { function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return byAccount.url + '/likes/' + video.id return byActor.url + '/likes/' + video.id
} }
function getVideoDislikeActivityPubUrl (byAccount: AccountModel, video: VideoModel) { function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return byAccount.url + '/dislikes/' + video.id return byActor.url + '/dislikes/' + video.id
} }
function getAccountFollowActivityPubUrl (accountFollow: AccountFollowModel) { function getActorFollowActivityPubUrl (actorFollow: ActorFollowModel) {
const me = accountFollow.AccountFollower const me = actorFollow.ActorFollower
const following = accountFollow.AccountFollowing const following = actorFollow.ActorFollowing
return me.url + '/follows/' + following.id return me.url + '/follows/' + following.id
} }
function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowModel) { function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
const follower = accountFollow.AccountFollower const follower = actorFollow.ActorFollower
const me = accountFollow.AccountFollowing const me = actorFollow.ActorFollowing
return follower.url + '/accepts/follows/' + me.id return follower.url + '/accepts/follows/' + me.id
} }
function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountModel) { function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) {
return originalUrl + '/announces/' + byAccount.id return originalUrl + '/announces/' + byActor.id
} }
function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) { function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
@ -60,12 +63,13 @@ function getUndoActivityPubUrl (originalUrl: string) {
} }
export { export {
getApplicationActivityPubUrl,
getVideoActivityPubUrl, getVideoActivityPubUrl,
getVideoChannelActivityPubUrl, getVideoChannelActivityPubUrl,
getAccountActivityPubUrl, getAccountActivityPubUrl,
getVideoAbuseActivityPubUrl, getVideoAbuseActivityPubUrl,
getAccountFollowActivityPubUrl, getActorFollowActivityPubUrl,
getAccountFollowAcceptActivityPubUrl, getActorFollowAcceptActivityPubUrl,
getAnnounceActivityPubUrl, getAnnounceActivityPubUrl,
getUpdateActivityPubUrl, getUpdateActivityPubUrl,
getUndoActivityPubUrl, getUndoActivityPubUrl,

View File

@ -1,59 +0,0 @@
import { VideoChannelObject } from '../../../shared/models/activitypub/objects'
import { doRequest, logger } from '../../helpers'
import { isVideoChannelObjectValid } from '../../helpers/custom-validators/activitypub'
import { ACTIVITY_PUB } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { VideoChannelModel } from '../../models/video/video-channel'
import { videoChannelActivityObjectToDBAttributes } from './process/misc'
async function getOrCreateVideoChannel (ownerAccount: AccountModel, videoChannelUrl: string) {
let videoChannel = await VideoChannelModel.loadByUrl(videoChannelUrl)
// We don't have this account in our database, fetch it on remote
if (!videoChannel) {
videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl)
if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.')
// Save our new video channel in database
await videoChannel.save()
}
return videoChannel
}
async function fetchRemoteVideoChannel (ownerAccount: AccountModel, videoChannelUrl: string) {
const options = {
uri: videoChannelUrl,
method: 'GET',
headers: {
'Accept': ACTIVITY_PUB.ACCEPT_HEADER
}
}
logger.info('Fetching remote video channel %s.', videoChannelUrl)
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err)
return undefined
}
const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body)
if (isVideoChannelObjectValid(videoChannelJSON) === false) {
logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON })
return undefined
}
const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount)
const videoChannel = new VideoChannelModel(videoChannelAttributes)
videoChannel.Account = ownerAccount
return videoChannel
}
export {
getOrCreateVideoChannel,
fetchRemoteVideoChannel
}

View File

@ -19,7 +19,7 @@ import {
function fetchRemoteVideoPreview (video: VideoModel) { function fetchRemoteVideoPreview (video: VideoModel) {
// FIXME: use url // FIXME: use url
const host = video.VideoChannel.Account.Server.host const host = video.VideoChannel.Account.Actor.Server.host
const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
return request.get(REMOTE_SCHEME.HTTP + '://' + host + path) return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
@ -27,7 +27,7 @@ function fetchRemoteVideoPreview (video: VideoModel) {
async function fetchRemoteVideoDescription (video: VideoModel) { async function fetchRemoteVideoDescription (video: VideoModel) {
// FIXME: use url // FIXME: use url
const host = video.VideoChannel.Account.Server.host const host = video.VideoChannel.Account.Actor.Server.host
const path = video.getDescriptionPath() const path = video.getDescriptionPath()
const options = { const options = {
uri: REMOTE_SCHEME.HTTP + '://' + host + path, uri: REMOTE_SCHEME.HTTP + '://' + host + path,
@ -50,43 +50,47 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
} }
async function sendVideoRateChangeToFollowers ( async function sendVideoRateChangeToFollowers (
account: AccountModel, account: AccountModel,
video: VideoModel, video: VideoModel,
likes: number, likes: number,
dislikes: number, dislikes: number,
t: Transaction t: Transaction
) { ) {
const actor = account.Actor
// Keep the order: first we undo and then we create // Keep the order: first we undo and then we create
// Undo Like // Undo Like
if (likes < 0) await sendUndoLikeToVideoFollowers(account, video, t) if (likes < 0) await sendUndoLikeToVideoFollowers(actor, video, t)
// Undo Dislike // Undo Dislike
if (dislikes < 0) await sendUndoDislikeToVideoFollowers(account, video, t) if (dislikes < 0) await sendUndoDislikeToVideoFollowers(actor, video, t)
// Like // Like
if (likes > 0) await sendLikeToVideoFollowers(account, video, t) if (likes > 0) await sendLikeToVideoFollowers(actor, video, t)
// Dislike // Dislike
if (dislikes > 0) await sendCreateDislikeToVideoFollowers(account, video, t) if (dislikes > 0) await sendCreateDislikeToVideoFollowers(actor, video, t)
} }
async function sendVideoRateChangeToOrigin ( async function sendVideoRateChangeToOrigin (
account: AccountModel, account: AccountModel,
video: VideoModel, video: VideoModel,
likes: number, likes: number,
dislikes: number, dislikes: number,
t: Transaction t: Transaction
) { ) {
const actor = account.Actor
// Keep the order: first we undo and then we create // Keep the order: first we undo and then we create
// Undo Like // Undo Like
if (likes < 0) await sendUndoLikeToOrigin(account, video, t) if (likes < 0) await sendUndoLikeToOrigin(actor, video, t)
// Undo Dislike // Undo Dislike
if (dislikes < 0) await sendUndoDislikeToOrigin(account, video, t) if (dislikes < 0) await sendUndoDislikeToOrigin(actor, video, t)
// Like // Like
if (likes > 0) await sendLikeToOrigin(account, video, t) if (likes > 0) await sendLikeToOrigin(actor, video, t)
// Dislike // Dislike
if (dislikes > 0) await sendCreateDislikeToOrigin(account, video, t) if (dislikes > 0) await sendCreateDislikeToOrigin(actor, video, t)
} }
export { export {

View File

@ -1,6 +0,0 @@
export * from './activitypub'
export * from './cache'
export * from './jobs'
export * from './oauth-model'
export * from './user'
export * from './video-channel'

View File

@ -1,7 +1,7 @@
import { JobCategory } from '../../../../shared' import { JobCategory } from '../../../../shared'
import { buildSignedActivity, logger } from '../../../helpers' import { buildSignedActivity, logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers' import { ACTIVITY_PUB } from '../../../initializers'
import { AccountModel } from '../../../models/account/account' import { ActorModel } from '../../../models/activitypub/actor'
import { JobHandler, JobScheduler } from '../job-scheduler' import { JobHandler, JobScheduler } from '../job-scheduler'
import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler' import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler'
@ -10,7 +10,7 @@ import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handl
type ActivityPubHttpPayload = { type ActivityPubHttpPayload = {
uris: string[] uris: string[]
signatureAccountId?: number signatureActorId?: number
body?: any body?: any
attemptNumber?: number attemptNumber?: number
} }
@ -44,10 +44,10 @@ function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, ur
async function computeBody (payload: ActivityPubHttpPayload) { async function computeBody (payload: ActivityPubHttpPayload) {
let body = payload.body let body = payload.body
if (payload.signatureAccountId) { if (payload.signatureActorId) {
const accountSignature = await AccountModel.load(payload.signatureAccountId) const actorSignature = await ActorModel.load(payload.signatureActorId)
if (!accountSignature) throw new Error('Unknown signature account id.') if (!actorSignature) throw new Error('Unknown signature account id.')
body = await buildSignedActivity(accountSignature, payload.body) body = await buildSignedActivity(actorSignature, payload.body)
} }
return body return body

View File

@ -3,7 +3,7 @@ import { computeResolutionsToTranscode, logger } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { shareVideoByServer } from '../../activitypub' import { shareVideoByServer } from '../../activitypub'
import { sendAddVideo } from '../../activitypub/send' import { sendCreateVideo } from '../../activitypub/send'
import { JobScheduler } from '../job-scheduler' import { JobScheduler } from '../job-scheduler'
import { TranscodingJobPayload } from './transcoding-job-scheduler' import { TranscodingJobPayload } from './transcoding-job-scheduler'
@ -36,7 +36,8 @@ async function onSuccess (jobId: number, video: VideoModel, jobScheduler: JobSch
if (!videoDatabase) return undefined if (!videoDatabase) return undefined
// Now we'll add the video's meta data to our followers // Now we'll add the video's meta data to our followers
await sendAddVideo(video, undefined) await sendCreateVideo(video, undefined)
// TODO: share by channel
await shareVideoByServer(video, undefined) await shareVideoByServer(video, undefined)
const originalFileHeight = await videoDatabase.getOriginalFileHeight() const originalFileHeight = await videoDatabase.getOriginalFileHeight()

View File

@ -1,10 +1,9 @@
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { createPrivateAndPublicKeys, logger } from '../helpers' import { ActivityPubActorType } from '../../shared/models/activitypub'
import { CONFIG, sequelizeTypescript } from '../initializers' import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../initializers'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor' import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
import { getAccountActivityPubUrl } from './activitypub'
import { createVideoChannel } from './video-channel' import { createVideoChannel } from './video-channel'
async function createUserAccountAndChannel (user: UserModel, validateUser = true) { async function createUserAccountAndChannel (user: UserModel, validateUser = true) {
@ -26,31 +25,22 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true
return { account: accountCreated, videoChannel } return { account: accountCreated, videoChannel }
}) })
// Set account keys, this could be long so process after the account creation and do not block the client account.Actor = await setAsyncActorKeys(account.Actor)
const { publicKey, privateKey } = await createPrivateAndPublicKeys() videoChannel.Actor = await setAsyncActorKeys(videoChannel.Actor)
const actor = account.Actor
actor.set('publicKey', publicKey)
actor.set('privateKey', privateKey)
actor.save().catch(err => logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err))
return { account, videoChannel } return { account, videoChannel }
} }
async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) { async function createLocalAccountWithoutKeys (
name: string,
userId: number,
applicationId: number,
t: Sequelize.Transaction,
type: ActivityPubActorType= 'Person'
) {
const url = getAccountActivityPubUrl(name) const url = getAccountActivityPubUrl(name)
const actorInstance = new ActorModel({ const actorInstance = buildActorInstance(type, url, name)
url,
publicKey: null,
privateKey: null,
followersCount: 0,
followingCount: 0,
inboxUrl: url + '/inbox',
outboxUrl: url + '/outbox',
sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
followersUrl: url + '/followers',
followingUrl: url + '/following'
})
const actorInstanceCreated = await actorInstance.save({ transaction: t }) const actorInstanceCreated = await actorInstance.save({ transaction: t })
const accountInstance = new AccountModel({ const accountInstance = new AccountModel({
@ -67,9 +57,18 @@ async function createLocalAccountWithoutKeys (name: string, userId: number, appl
return accountInstanceCreated return accountInstanceCreated
} }
async function createApplicationActor (applicationId: number) {
const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationId, undefined, 'Application')
accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor)
return accountCreated
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
createApplicationActor,
createUserAccountAndChannel, createUserAccountAndChannel,
createLocalAccountWithoutKeys createLocalAccountWithoutKeys
} }

View File

@ -1,26 +1,33 @@
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import * as uuidv4 from 'uuid/v4'
import { VideoChannelCreate } from '../../shared/models' import { VideoChannelCreate } from '../../shared/models'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { VideoChannelModel } from '../models/video/video-channel' import { VideoChannelModel } from '../models/video/video-channel'
import { getVideoChannelActivityPubUrl } from './activitypub' import { buildActorInstance, getVideoChannelActivityPubUrl } from './activitypub'
async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) { async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) {
const uuid = uuidv4()
const url = getVideoChannelActivityPubUrl(uuid)
// We use the name as uuid
const actorInstance = buildActorInstance('Group', url, uuid, uuid)
const actorInstanceCreated = await actorInstance.save({ transaction: t })
const videoChannelData = { const videoChannelData = {
name: videoChannelInfo.name, name: videoChannelInfo.name,
description: videoChannelInfo.description, description: videoChannelInfo.description,
remote: false, accountId: account.id,
accountId: account.id actorId: actorInstanceCreated.id
} }
const videoChannel = VideoChannelModel.build(videoChannelData) const videoChannel = VideoChannelModel.build(videoChannelData)
videoChannel.set('url', getVideoChannelActivityPubUrl(videoChannel))
const options = { transaction: t } const options = { transaction: t }
const videoChannelCreated = await videoChannel.save(options) const videoChannelCreated = await videoChannel.save(options)
// Do not forget to add Account information to the created video channel // Do not forget to add Account/Actor information to the created video channel
videoChannelCreated.Account = account videoChannelCreated.Account = account
videoChannelCreated.Actor = actorInstanceCreated
// No need to seed this empty video channel to followers // No need to seed this empty video channel to followers
return videoChannelCreated return videoChannelCreated

View File

@ -3,33 +3,27 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'
import { ActivityPubSignature } from '../../shared' import { ActivityPubSignature } from '../../shared'
import { isSignatureVerified, logger } from '../helpers' import { isSignatureVerified, logger } from '../helpers'
import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers' import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers'
import { fetchRemoteAccount, saveAccountAndServerIfNotExist } from '../lib/activitypub' import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
import { AccountModel } from '../models/account/account' import { ActorModel } from '../models/activitypub/actor'
async function checkSignature (req: Request, res: Response, next: NextFunction) { async function checkSignature (req: Request, res: Response, next: NextFunction) {
const signatureObject: ActivityPubSignature = req.body.signature const signatureObject: ActivityPubSignature = req.body.signature
logger.debug('Checking signature of account %s...', signatureObject.creator) logger.debug('Checking signature of actor %s...', signatureObject.creator)
let account = await AccountModel.loadByUrl(signatureObject.creator) let actor: ActorModel
try {
// We don't have this account in our database, fetch it on remote actor = await getOrCreateActorAndServerAndModel(signatureObject.creator)
if (!account) { } catch (err) {
account = await fetchRemoteAccount(signatureObject.creator) logger.error('Cannot create remote actor and check signature.', err)
return res.sendStatus(403)
if (!account) {
return res.sendStatus(403)
}
// Save our new account and its server in database
await saveAccountAndServerIfNotExist(account)
} }
const verified = await isSignatureVerified(account, req.body) const verified = await isSignatureVerified(actor, req.body)
if (verified === false) return res.sendStatus(403) if (verified === false) return res.sendStatus(403)
res.locals.signature = { res.locals.signature = {
account actor
} }
return next() return next()

View File

@ -1,10 +1,9 @@
import * as express from 'express' import * as express from 'express'
import { body, param } from 'express-validator/check' import { body, param } from 'express-validator/check'
import { getServerAccount, isTestInstance, logger } from '../../helpers' import { getServerActor, isTestInstance, logger } from '../../helpers'
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
import { isEachUniqueHostValid } from '../../helpers/custom-validators/servers'
import { CONFIG } from '../../initializers' import { CONFIG } from '../../initializers'
import { AccountFollowModel } from '../../models/account/account-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
const followValidator = [ const followValidator = [
@ -29,15 +28,15 @@ const followValidator = [
] ]
const removeFollowingValidator = [ const removeFollowingValidator = [
param('accountId').custom(isIdOrUUIDValid).withMessage('Should have a valid account id'), param('host').custom(isHostValid).withMessage('Should have a valid host'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking unfollow parameters', { parameters: req.params }) logger.debug('Checking unfollow parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
const serverAccount = await getServerAccount() const serverActor = await getServerActor()
const follow = await AccountFollowModel.loadByAccountAndTarget(serverAccount.id, req.params.accountId) const follow = await ActorFollowModel.loadByActorAndTargetHost(serverActor.id, req.params.host)
if (!follow) { if (!follow) {
return res.status(404) return res.status(404)

View File

@ -3,7 +3,7 @@ import { body, param } from 'express-validator/check'
import { UserRight } from '../../../shared' import { UserRight } from '../../../shared'
import { logger } from '../../helpers' import { logger } from '../../helpers'
import { isAccountIdExist } from '../../helpers/custom-validators/accounts' import { isAccountIdExist } from '../../helpers/custom-validators/accounts'
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import { import {
isVideoChannelDescriptionValid, isVideoChannelDescriptionValid,
isVideoChannelExist, isVideoChannelExist,
@ -11,7 +11,6 @@ import {
} from '../../helpers/custom-validators/video-channels' } from '../../helpers/custom-validators/video-channels'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoChannelShareModel } from '../../models/video/video-channel-share'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
const listVideoAccountChannelsValidator = [ const listVideoAccountChannelsValidator = [
@ -98,28 +97,6 @@ const videoChannelsGetValidator = [
} }
] ]
const videoChannelsShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoChannelShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoChannelExist(req.params.id, res)) return
const share = await VideoChannelShareModel.load(res.locals.video.id, req.params.accountId, undefined)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoChannelShare = share
return next()
}
]
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -127,15 +104,13 @@ export {
videoChannelsAddValidator, videoChannelsAddValidator,
videoChannelsUpdateValidator, videoChannelsUpdateValidator,
videoChannelsRemoveValidator, videoChannelsRemoveValidator,
videoChannelsGetValidator, videoChannelsGetValidator
videoChannelsShareValidator
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) { function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) {
// Retrieve the user who did the request if (videoChannel.Actor.isOwned() === false) {
if (videoChannel.isOwned() === false) {
res.status(403) res.status(403)
.json({ error: 'Cannot remove video channel of another server.' }) .json({ error: 'Cannot remove video channel of another server.' })
.end() .end()

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { query } from 'express-validator/check' import { query } from 'express-validator/check'
import { logger } from '../../helpers' import { logger } from '../../helpers'
import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger' import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger'
import { AccountModel } from '../../models/account/account' import { ActorModel } from '../../models/activitypub/actor'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
const webfingerValidator = [ const webfingerValidator = [
@ -17,14 +17,14 @@ const webfingerValidator = [
const nameWithHost = req.query.resource.substr(5) const nameWithHost = req.query.resource.substr(5)
const [ name ] = nameWithHost.split('@') const [ name ] = nameWithHost.split('@')
const account = await AccountModel.loadLocalByName(name) const actor = await ActorModel.loadLocalByName(name)
if (!account) { if (!actor) {
return res.status(404) return res.status(404)
.send({ error: 'Account not found' }) .send({ error: 'Actor not found' })
.end() .end()
} }
res.locals.account = account res.locals.actor = actor
return next() return next()
} }
] ]

View File

@ -1,228 +0,0 @@
import * as Bluebird from 'bluebird'
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { FollowState } from '../../../shared/models/accounts'
import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server'
import { getSort } from '../utils'
import { AccountModel } from './account'
@Table({
tableName: 'accountFollow',
indexes: [
{
fields: [ 'accountId' ]
},
{
fields: [ 'targetAccountId' ]
},
{
fields: [ 'accountId', 'targetAccountId' ],
unique: true
}
]
})
export class AccountFollowModel extends Model<AccountFollowModel> {
@AllowNull(false)
@Column(DataType.ENUM(values(FOLLOW_STATES)))
state: FollowState
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@ForeignKey(() => AccountModel)
@Column
accountId: number
@BelongsTo(() => AccountModel, {
foreignKey: {
name: 'accountId',
allowNull: false
},
as: 'AccountFollower',
onDelete: 'CASCADE'
})
AccountFollower: AccountModel
@ForeignKey(() => AccountModel)
@Column
targetAccountId: number
@BelongsTo(() => AccountModel, {
foreignKey: {
name: 'targetAccountId',
allowNull: false
},
as: 'AccountFollowing',
onDelete: 'CASCADE'
})
AccountFollowing: AccountModel
static loadByAccountAndTarget (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
const query = {
where: {
accountId,
targetAccountId
},
include: [
{
model: AccountModel,
required: true,
as: 'AccountFollower'
},
{
model: AccountModel,
required: true,
as: 'AccountFollowing'
}
],
transaction: t
}
return AccountFollowModel.findOne(query)
}
static listFollowingForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
offset: start,
limit: count,
order: [ getSort(sort) ],
include: [
{
model: AccountModel,
required: true,
as: 'AccountFollower',
where: {
id
}
},
{
model: AccountModel,
as: 'AccountFollowing',
required: true,
include: [ ServerModel ]
}
]
}
return AccountFollowModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows,
total: count
}
})
}
static listFollowersForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
offset: start,
limit: count,
order: [ getSort(sort) ],
include: [
{
model: AccountModel,
required: true,
as: 'AccountFollower',
include: [ ServerModel ]
},
{
model: AccountModel,
as: 'AccountFollowing',
required: true,
where: {
id
}
}
]
}
return AccountFollowModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows,
total: count
}
})
}
static listAcceptedFollowerUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count)
}
static listAcceptedFollowerSharedInboxUrls (accountIds: number[], t: Sequelize.Transaction) {
return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl')
}
static listAcceptedFollowingUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
return AccountFollowModel.createListAcceptedFollowForApiQuery('following', accountIds, t, start, count)
}
private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
accountIds: number[],
t: Sequelize.Transaction,
start?: number,
count?: number,
columnUrl = 'url') {
let firstJoin: string
let secondJoin: string
if (type === 'followers') {
firstJoin = 'targetAccountId'
secondJoin = 'accountId'
} else {
firstJoin = 'accountId'
secondJoin = 'targetAccountId'
}
const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
const tasks: Bluebird<any>[] = []
for (const selection of selections) {
let query = 'SELECT ' + selection + ' FROM "account" ' +
'INNER JOIN "accountFollow" ON "accountFollow"."' + firstJoin + '" = "account"."id" ' +
'INNER JOIN "account" AS "Follows" ON "accountFollow"."' + secondJoin + '" = "Follows"."id" ' +
'WHERE "account"."id" = ANY ($accountIds) AND "accountFollow"."state" = \'accepted\' '
if (count !== undefined) query += 'LIMIT ' + count
if (start !== undefined) query += ' OFFSET ' + start
const options = {
bind: { accountIds },
type: Sequelize.QueryTypes.SELECT,
transaction: t
}
tasks.push(AccountFollowModel.sequelize.query(query, options))
}
const [ followers, [ { total } ] ] = await
Promise.all(tasks)
const urls: string[] = followers.map(f => f.url)
return {
data: urls,
total: parseInt(total, 10)
}
}
toFormattedJSON () {
const follower = this.AccountFollower.toFormattedJSON()
const following = this.AccountFollowing.toFormattedJSON()
return {
id: this.id,
follower,
following,
state: this.state,
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
}
}

View File

@ -5,18 +5,16 @@ import {
BelongsTo, BelongsTo,
Column, Column,
CreatedAt, CreatedAt,
DataType, DefaultScope,
Default,
ForeignKey, ForeignKey,
HasMany, HasMany,
Is, Is,
IsUUID,
Model, Model,
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { isUserUsernameValid } from '../../helpers/custom-validators/users' import { isUserUsernameValid } from '../../helpers/custom-validators/users'
import { sendDeleteAccount } from '../../lib/activitypub/send' import { sendDeleteActor } from '../../lib/activitypub/send'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { ApplicationModel } from '../application/application' import { ApplicationModel } from '../application/application'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
@ -24,31 +22,30 @@ import { throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel' import { VideoChannelModel } from '../video/video-channel'
import { UserModel } from './user' import { UserModel } from './user'
@Table({ @DefaultScope({
tableName: 'account', include: [
indexes: [
{ {
fields: [ 'name' ] model: () => ActorModel,
}, required: true,
{ include: [
fields: [ 'serverId' ] {
}, model: () => ServerModel,
{ required: false
fields: [ 'userId' ], }
unique: true ]
},
{
fields: [ 'applicationId' ],
unique: true
},
{
fields: [ 'name', 'serverId', 'applicationId' ],
unique: true
} }
] ]
}) })
@Table({
tableName: 'account'
})
export class AccountModel extends Model<AccountModel> { export class AccountModel extends Model<AccountModel> {
@AllowNull(false)
@Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
@Column
name: string
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -89,7 +86,7 @@ export class AccountModel extends Model<AccountModel> {
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
Application: ApplicationModel Account: ApplicationModel
@HasMany(() => VideoChannelModel, { @HasMany(() => VideoChannelModel, {
foreignKey: { foreignKey: {
@ -103,32 +100,27 @@ export class AccountModel extends Model<AccountModel> {
@AfterDestroy @AfterDestroy
static sendDeleteIfOwned (instance: AccountModel) { static sendDeleteIfOwned (instance: AccountModel) {
if (instance.isOwned()) { if (instance.isOwned()) {
return sendDeleteAccount(instance, undefined) return sendDeleteActor(instance.Actor, undefined)
} }
return undefined return undefined
} }
static loadApplication () {
return AccountModel.findOne({
include: [
{
model: ApplicationModel,
required: true
}
]
})
}
static load (id: number) { static load (id: number) {
return AccountModel.findById(id) return AccountModel.findById(id)
} }
static loadByUUID (uuid: string) { static loadByUUID (uuid: string) {
const query = { const query = {
where: { include: [
uuid {
} model: ActorModel,
required: true,
where: {
uuid
}
}
]
} }
return AccountModel.findOne(query) return AccountModel.findOne(query)
@ -156,25 +148,6 @@ export class AccountModel extends Model<AccountModel> {
return AccountModel.findOne(query) return AccountModel.findOne(query)
} }
static loadByNameAndHost (name: string, host: string) {
const query = {
where: {
name
},
include: [
{
model: ServerModel,
required: true,
where: {
host
}
}
]
}
return AccountModel.findOne(query)
}
static loadByUrl (url: string, transaction?: Sequelize.Transaction) { static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
const query = { const query = {
include: [ include: [
@ -192,29 +165,11 @@ export class AccountModel extends Model<AccountModel> {
return AccountModel.findOne(query) return AccountModel.findOne(query)
} }
static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query = {
include: [
{
model: ActorModel,
required: true,
where: {
followersUrl: {
[ Sequelize.Op.in ]: followersUrls
}
}
}
],
transaction
}
return AccountModel.findAll(query)
}
toFormattedJSON () { toFormattedJSON () {
const actor = this.Actor.toFormattedJSON() const actor = this.Actor.toFormattedJSON()
const account = { const account = {
id: this.id, id: this.id,
name: this.name,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt updatedAt: this.updatedAt
} }
@ -223,7 +178,7 @@ export class AccountModel extends Model<AccountModel> {
} }
toActivityPubObject () { toActivityPubObject () {
return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account') return this.Actor.toActivityPubObject(this.name, 'Account')
} }
isOwned () { isOwned () {

View File

@ -1,26 +1,13 @@
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { import {
AllowNull, AllowNull, BeforeCreate, BeforeUpdate, Column, CreatedAt, DataType, Default, DefaultScope, HasMany, HasOne, Is, IsEmail, Model,
BeforeCreate, Scopes, Table, UpdatedAt
BeforeUpdate,
Column, CreatedAt,
DataType,
Default, DefaultScope,
HasMany,
HasOne,
Is,
IsEmail,
Model, Scopes,
Table, UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
import { comparePassword, cryptPassword } from '../../helpers'
import { import {
comparePassword, isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
cryptPassword isUserVideoQuotaValid
} from '../../helpers'
import {
isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
isUserVideoQuotaValid, isUserAutoPlayVideoValid
} from '../../helpers/custom-validators/users' } from '../../helpers/custom-validators/users'
import { OAuthTokenModel } from '../oauth/oauth-token' import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'

View File

@ -0,0 +1,260 @@
import * as Bluebird from 'bluebird'
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { FollowState } from '../../../shared/models/actors'
import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server'
import { getSort } from '../utils'
import { ActorModel } from './actor'
@Table({
tableName: 'actorFollow',
indexes: [
{
fields: [ 'actorId' ]
},
{
fields: [ 'targetActorId' ]
},
{
fields: [ 'actorId', 'targetActorId' ],
unique: true
}
]
})
export class ActorFollowModel extends Model<ActorFollowModel> {
@AllowNull(false)
@Column(DataType.ENUM(values(FOLLOW_STATES)))
state: FollowState
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@ForeignKey(() => ActorModel)
@Column
actorId: number
@BelongsTo(() => ActorModel, {
foreignKey: {
name: 'actorId',
allowNull: false
},
as: 'ActorFollower',
onDelete: 'CASCADE'
})
ActorFollower: ActorModel
@ForeignKey(() => ActorModel)
@Column
targetActorId: number
@BelongsTo(() => ActorModel, {
foreignKey: {
name: 'targetActorId',
allowNull: false
},
as: 'ActorFollowing',
onDelete: 'CASCADE'
})
ActorFollowing: ActorModel
static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
const query = {
where: {
actorId,
targetActorId: targetActorId
},
include: [
{
model: ActorModel,
required: true,
as: 'ActorFollower'
},
{
model: ActorModel,
required: true,
as: 'ActorFollowing'
}
],
transaction: t
}
return ActorFollowModel.findOne(query)
}
static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
const query = {
where: {
actorId
},
include: [
{
model: ActorModel,
required: true,
as: 'ActorFollower'
},
{
model: ActorModel,
required: true,
as: 'ActorFollowing',
include: [
{
model: ServerModel,
required: true,
where: {
host: targetHost
}
}
]
}
],
transaction: t
}
return ActorFollowModel.findOne(query)
}
static listFollowingForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
offset: start,
limit: count,
order: [ getSort(sort) ],
include: [
{
model: ActorModel,
required: true,
as: 'ActorFollower',
where: {
id
}
},
{
model: ActorModel,
as: 'ActorFollowing',
required: true,
include: [ ServerModel ]
}
]
}
return ActorFollowModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows,
total: count
}
})
}
static listFollowersForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
offset: start,
limit: count,
order: [ getSort(sort) ],
include: [
{
model: ActorModel,
required: true,
as: 'ActorFollower',
include: [ ServerModel ]
},
{
model: ActorModel,
as: 'ActorFollowing',
required: true,
where: {
id
}
}
]
}
return ActorFollowModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows,
total: count
}
})
}
static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
}
static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, undefined, undefined, 'sharedInboxUrl')
}
static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
}
private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
actorIds: number[],
t: Sequelize.Transaction,
start?: number,
count?: number,
columnUrl = 'url') {
let firstJoin: string
let secondJoin: string
if (type === 'followers') {
firstJoin = 'targetActorId'
secondJoin = 'actorId'
} else {
firstJoin = 'actorId'
secondJoin = 'targetActorId'
}
const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
const tasks: Bluebird<any>[] = []
for (const selection of selections) {
let query = 'SELECT ' + selection + ' FROM "actor" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
if (count !== undefined) query += 'LIMIT ' + count
if (start !== undefined) query += ' OFFSET ' + start
const options = {
bind: { actorIds },
type: Sequelize.QueryTypes.SELECT,
transaction: t
}
tasks.push(ActorFollowModel.sequelize.query(query, options))
}
const [ followers, [ { total } ] ] = await
Promise.all(tasks)
const urls: string[] = followers.map(f => f.url)
return {
data: urls,
total: parseInt(total, 10)
}
}
toFormattedJSON () {
const follower = this.ActorFollower.toFormattedJSON()
const following = this.ActorFollowing.toFormattedJSON()
return {
id: this.id,
follower,
following,
state: this.state,
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
}
}

View File

@ -1,30 +1,75 @@
import { values } from 'lodash'
import { join } from 'path' import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { import {
AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table, AllowNull,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
ForeignKey,
HasMany,
HasOne,
Is,
IsUUID,
Model,
Scopes,
Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { activityPubContextify } from '../../helpers' import { activityPubContextify } from '../../helpers'
import { import {
isActivityPubUrlValid, isActivityPubUrlValid,
isActorFollowersCountValid, isActorFollowersCountValid,
isActorFollowingCountValid, isActorPreferredUsernameValid, isActorFollowingCountValid,
isActorNameValid,
isActorPrivateKeyValid, isActorPrivateKeyValid,
isActorPublicKeyValid isActorPublicKeyValid
} from '../../helpers/custom-validators/activitypub' } from '../../helpers/custom-validators/activitypub'
import { isUserUsernameValid } from '../../helpers/custom-validators/users' import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { AccountModel } from '../account/account'
import { AccountFollowModel } from '../account/account-follow'
import { AvatarModel } from '../avatar/avatar' import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { throwIfNotValid } from '../utils' import { throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel'
import { ActorFollowModel } from './actor-follow'
enum ScopeNames {
FULL = 'FULL'
}
@Scopes({
[ScopeNames.FULL]: {
include: [
{
model: () => AccountModel,
required: false
},
{
model: () => VideoChannelModel,
required: false
}
]
}
})
@Table({ @Table({
tableName: 'actor' tableName: 'actor',
indexes: [
{
fields: [ 'name', 'serverId' ],
unique: true
}
]
}) })
export class ActorModel extends Model<ActorModel> { export class ActorModel extends Model<ActorModel> {
@AllowNull(false)
@Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
type: ActivityPubActorType
@AllowNull(false) @AllowNull(false)
@Default(DataType.UUIDV4) @Default(DataType.UUIDV4)
@IsUUID(4) @IsUUID(4)
@ -32,7 +77,7 @@ export class ActorModel extends Model<ActorModel> {
uuid: string uuid: string
@AllowNull(false) @AllowNull(false)
@Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name')) @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name'))
@Column @Column
name: string name: string
@ -104,24 +149,24 @@ export class ActorModel extends Model<ActorModel> {
}) })
Avatar: AvatarModel Avatar: AvatarModel
@HasMany(() => AccountFollowModel, { @HasMany(() => ActorFollowModel, {
foreignKey: { foreignKey: {
name: 'accountId', name: 'actorId',
allowNull: false allowNull: false
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
AccountFollowing: AccountFollowModel[] AccountFollowing: ActorFollowModel[]
@HasMany(() => AccountFollowModel, { @HasMany(() => ActorFollowModel, {
foreignKey: { foreignKey: {
name: 'targetAccountId', name: 'targetActorId',
allowNull: false allowNull: false
}, },
as: 'followers', as: 'followers',
onDelete: 'cascade' onDelete: 'cascade'
}) })
AccountFollowers: AccountFollowModel[] AccountFollowers: ActorFollowModel[]
@ForeignKey(() => ServerModel) @ForeignKey(() => ServerModel)
@Column @Column
@ -135,6 +180,36 @@ export class ActorModel extends Model<ActorModel> {
}) })
Server: ServerModel Server: ServerModel
@HasOne(() => AccountModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
Account: AccountModel
@HasOne(() => VideoChannelModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
VideoChannel: VideoChannelModel
static load (id: number) {
return ActorModel.scope(ScopeNames.FULL).findById(id)
}
static loadByUUID (uuid: string) {
const query = {
where: {
uuid
}
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
}
static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query = { const query = {
where: { where: {
@ -145,7 +220,48 @@ export class ActorModel extends Model<ActorModel> {
transaction transaction
} }
return ActorModel.findAll(query) return ActorModel.scope(ScopeNames.FULL).findAll(query)
}
static loadLocalByName (name: string) {
const query = {
where: {
name,
serverId: null
}
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
}
static loadByNameAndHost (name: string, host: string) {
const query = {
where: {
name
},
include: [
{
model: ServerModel,
required: true,
where: {
host
}
}
]
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
}
static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
const query = {
where: {
url
},
transaction
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
} }
toFormattedJSON () { toFormattedJSON () {
@ -167,6 +283,7 @@ export class ActorModel extends Model<ActorModel> {
return { return {
id: this.id, id: this.id,
uuid: this.uuid,
host, host,
score, score,
followingCount: this.followingCount, followingCount: this.followingCount,
@ -175,28 +292,30 @@ export class ActorModel extends Model<ActorModel> {
} }
} }
toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') { toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') {
let activityPubType let activityPubType
if (type === 'Account') { if (type === 'Account') {
activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' activityPubType = 'Person' as 'Person'
} else if (type === 'Application') {
activityPubType = 'Application' as 'Application'
} else { // VideoChannel } else { // VideoChannel
activityPubType = 'Group' activityPubType = 'Group' as 'Group'
} }
const json = { const json = {
type, type: activityPubType,
id: this.url, id: this.url,
following: this.getFollowingUrl(), following: this.getFollowingUrl(),
followers: this.getFollowersUrl(), followers: this.getFollowersUrl(),
inbox: this.inboxUrl, inbox: this.inboxUrl,
outbox: this.outboxUrl, outbox: this.outboxUrl,
preferredUsername: name, preferredUsername,
url: this.url, url: this.url,
name, name: this.name,
endpoints: { endpoints: {
sharedInbox: this.sharedInboxUrl sharedInbox: this.sharedInboxUrl
}, },
uuid, uuid: this.uuid,
publicKey: { publicKey: {
id: this.getPublicKeyUrl(), id: this.getPublicKeyUrl(),
owner: this.url, owner: this.url,
@ -212,11 +331,11 @@ export class ActorModel extends Model<ActorModel> {
attributes: [ 'sharedInboxUrl' ], attributes: [ 'sharedInboxUrl' ],
include: [ include: [
{ {
model: AccountFollowModel, model: ActorFollowModel,
required: true, required: true,
as: 'followers', as: 'followers',
where: { where: {
targetAccountId: this.id targetActorId: this.id
} }
} }
], ],

View File

@ -1,5 +1,14 @@
import { AllowNull, Column, Default, IsInt, Model, Table } from 'sequelize-typescript' import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
import { AccountModel } from '../account/account'
@DefaultScope({
include: [
{
model: () => AccountModel,
required: true
}
]
})
@Table({ @Table({
tableName: 'application' tableName: 'application'
}) })
@ -11,7 +20,19 @@ export class ApplicationModel extends Model<ApplicationModel> {
@Column @Column
migrationVersion: number migrationVersion: number
@HasOne(() => AccountModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
Account: AccountModel
static countTotal () { static countTotal () {
return ApplicationModel.count() return ApplicationModel.count()
} }
static load () {
return ApplicationModel.findOne()
}
} }

View File

@ -3,7 +3,6 @@ import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos'
import { CONFIG } from '../../initializers' import { CONFIG } from '../../initializers'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
@ -63,13 +62,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
include: [ include: [
{ {
model: AccountModel, model: AccountModel,
required: true, required: true
include: [
{
model: ServerModel,
required: false
}
]
}, },
{ {
model: VideoModel, model: VideoModel,
@ -87,8 +80,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
toFormattedJSON () { toFormattedJSON () {
let reporterServerHost let reporterServerHost
if (this.Account.Server) { if (this.Account.Actor.Server) {
reporterServerHost = this.Account.Server.host reporterServerHost = this.Account.Actor.Server.host
} else { } else {
// It means it's our video // It means it's our video
reporterServerHost = CONFIG.WEBSERVER.HOST reporterServerHost = CONFIG.WEBSERVER.HOST

View File

@ -1,96 +0,0 @@
import * as Sequelize from 'sequelize'
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { AccountModel } from '../account/account'
import { VideoChannelModel } from './video-channel'
enum ScopeNames {
FULL = 'FULL',
WITH_ACCOUNT = 'WITH_ACCOUNT'
}
@Scopes({
[ScopeNames.FULL]: {
include: [
{
model: () => AccountModel,
required: true
},
{
model: () => VideoChannelModel,
required: true
}
]
},
[ScopeNames.WITH_ACCOUNT]: {
include: [
{
model: () => AccountModel,
required: true
}
]
}
})
@Table({
tableName: 'videoChannelShare',
indexes: [
{
fields: [ 'accountId' ]
},
{
fields: [ 'videoChannelId' ]
}
]
})
export class VideoChannelShareModel extends Model<VideoChannelShareModel> {
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@ForeignKey(() => AccountModel)
@Column
accountId: number
@BelongsTo(() => AccountModel, {
foreignKey: {
allowNull: false
},
onDelete: 'cascade'
})
Account: AccountModel
@ForeignKey(() => VideoChannelModel)
@Column
videoChannelId: number
@BelongsTo(() => VideoChannelModel, {
foreignKey: {
allowNull: false
},
onDelete: 'cascade'
})
VideoChannel: VideoChannelModel
static load (accountId: number, videoChannelId: number, t: Sequelize.Transaction) {
return VideoChannelShareModel.scope(ScopeNames.FULL).findOne({
where: {
accountId,
videoChannelId
},
transaction: t
})
}
static loadAccountsByShare (videoChannelId: number, t: Sequelize.Transaction) {
const query = {
where: {
videoChannelId
},
transaction: t
}
return VideoChannelShareModel.scope(ScopeNames.WITH_ACCOUNT).findAll(query)
.then(res => res.map(r => r.Account))
}
}

View File

@ -1,42 +1,52 @@
import * as Sequelize from 'sequelize'
import { import {
AfterDestroy, AfterDestroy,
AllowNull, AllowNull,
BelongsTo, BelongsTo,
Column, Column,
CreatedAt, CreatedAt,
DataType, DefaultScope,
Default,
ForeignKey, ForeignKey,
HasMany, HasMany,
Is, Is,
IsUUID,
Model, Model,
Scopes, Scopes,
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' import { ActivityPubActor } from '../../../shared/models/activitypub'
import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
import { sendDeleteVideoChannel } from '../../lib/activitypub/send' import { sendDeleteActor } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoChannelShareModel } from './video-channel-share'
enum ScopeNames { enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT', WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACTOR = 'WITH_ACTOR',
WITH_VIDEOS = 'WITH_VIDEOS' WITH_VIDEOS = 'WITH_VIDEOS'
} }
@DefaultScope({
include: [
{
model: () => ActorModel,
required: true
}
]
})
@Scopes({ @Scopes({
[ScopeNames.WITH_ACCOUNT]: { [ScopeNames.WITH_ACCOUNT]: {
include: [ include: [
{ {
model: () => AccountModel, model: () => AccountModel,
include: [ { model: () => ServerModel, required: false } ] required: true,
include: [
{
model: () => ActorModel,
required: true
}
]
} }
] ]
}, },
@ -44,6 +54,11 @@ enum ScopeNames {
include: [ include: [
() => VideoModel () => VideoModel
] ]
},
[ScopeNames.WITH_ACTOR]: {
include: [
() => ActorModel
]
} }
}) })
@Table({ @Table({
@ -56,12 +71,6 @@ enum ScopeNames {
}) })
export class VideoChannelModel extends Model<VideoChannelModel> { export class VideoChannelModel extends Model<VideoChannelModel> {
@AllowNull(false)
@Default(DataType.UUIDV4)
@IsUUID(4)
@Column(DataType.UUID)
uuid: string
@AllowNull(false) @AllowNull(false)
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
@Column @Column
@ -72,10 +81,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
@Column @Column
description: string description: string
@AllowNull(false)
@Column
remote: boolean
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -115,19 +120,10 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
}) })
Videos: VideoModel[] Videos: VideoModel[]
@HasMany(() => VideoChannelShareModel, {
foreignKey: {
name: 'channelId',
allowNull: false
},
onDelete: 'CASCADE'
})
VideoChannelShares: VideoChannelShareModel[]
@AfterDestroy @AfterDestroy
static sendDeleteIfOwned (instance: VideoChannelModel) { static sendDeleteIfOwned (instance: VideoChannelModel) {
if (instance.isOwned()) { if (instance.Actor.isOwned()) {
return sendDeleteVideoChannel(instance, undefined) return sendDeleteActor(instance.Actor, undefined)
} }
return undefined return undefined
@ -150,7 +146,9 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
order: [ getSort(sort) ] order: [ getSort(sort) ]
} }
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findAndCountAll(query) return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
.findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { total: count, data: rows } return { total: count, data: rows }
}) })
@ -165,51 +163,18 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
where: { where: {
id: accountId id: accountId
}, },
required: true, required: true
include: [ { model: ServerModel, required: false } ]
} }
] ]
} }
return VideoChannelModel.findAndCountAll(query) return VideoChannelModel
.findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { total: count, data: rows } return { total: count, data: rows }
}) })
} }
static loadByUrl (url: string, t?: Sequelize.Transaction) {
const query: IFindOptions<VideoChannelModel> = {
include: [
{
model: ActorModel,
required: true,
where: {
url
}
}
]
}
if (t !== undefined) query.transaction = t
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
}
static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) {
const query: IFindOptions<VideoChannelModel> = {
where: {
[ Sequelize.Op.or ]: [
{ uuid },
{ url }
]
}
}
if (t !== undefined) query.transaction = t
return VideoChannelModel.findOne(query)
}
static loadByIdAndAccount (id: number, accountId: number) { static loadByIdAndAccount (id: number, accountId: number) {
const options = { const options = {
where: { where: {
@ -218,21 +183,33 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
} }
} }
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(options) return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
.findOne(options)
} }
static loadAndPopulateAccount (id: number) { static loadAndPopulateAccount (id: number) {
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findById(id) return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
.findById(id)
} }
static loadByUUIDAndPopulateAccount (uuid: string) { static loadByUUIDAndPopulateAccount (uuid: string) {
const options = { const options = {
where: { include: [
uuid {
} model: ActorModel,
required: true,
where: {
uuid
}
}
]
} }
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(options) return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
.findOne(options)
} }
static loadAndPopulateAccountAndVideos (id: number) { static loadAndPopulateAccountAndVideos (id: number) {
@ -242,39 +219,36 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
] ]
} }
return VideoChannelModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]).findById(id, options) return VideoChannelModel
} .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
.findById(id, options)
isOwned () {
return this.remote === false
} }
toFormattedJSON () { toFormattedJSON () {
const json = { const actor = this.Actor.toFormattedJSON()
const account = {
id: this.id, id: this.id,
uuid: this.uuid,
name: this.name, name: this.name,
description: this.description, description: this.description,
isLocal: this.isOwned(), isLocal: this.Actor.isOwned(),
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt updatedAt: this.updatedAt
} }
if (this.Account !== undefined) { return Object.assign(actor, account)
json[ 'owner' ] = {
name: this.Account.name,
uuid: this.Account.uuid
}
}
if (Array.isArray(this.Videos)) {
json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON())
}
return json
} }
toActivityPubObject () { toActivityPubObject (): ActivityPubActor {
return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel') const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
return Object.assign(obj, {
summary: this.description,
attributedTo: [
{
type: 'Person' as 'Person',
id: this.Account.Actor.url
}
]
})
} }
} }

View File

@ -1,18 +1,18 @@
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor'
import { VideoModel } from './video' import { VideoModel } from './video'
enum ScopeNames { enum ScopeNames {
FULL = 'FULL', FULL = 'FULL',
WITH_ACCOUNT = 'WITH_ACCOUNT' WITH_ACTOR = 'WITH_ACTOR'
} }
@Scopes({ @Scopes({
[ScopeNames.FULL]: { [ScopeNames.FULL]: {
include: [ include: [
{ {
model: () => AccountModel, model: () => ActorModel,
required: true required: true
}, },
{ {
@ -21,10 +21,10 @@ enum ScopeNames {
} }
] ]
}, },
[ScopeNames.WITH_ACCOUNT]: { [ScopeNames.WITH_ACTOR]: {
include: [ include: [
{ {
model: () => AccountModel, model: () => ActorModel,
required: true required: true
} }
] ]
@ -34,7 +34,7 @@ enum ScopeNames {
tableName: 'videoShare', tableName: 'videoShare',
indexes: [ indexes: [
{ {
fields: [ 'accountId' ] fields: [ 'actorId' ]
}, },
{ {
fields: [ 'videoId' ] fields: [ 'videoId' ]
@ -48,17 +48,17 @@ export class VideoShareModel extends Model<VideoShareModel> {
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
@ForeignKey(() => AccountModel) @ForeignKey(() => ActorModel)
@Column @Column
accountId: number actorId: number
@BelongsTo(() => AccountModel, { @BelongsTo(() => ActorModel, {
foreignKey: { foreignKey: {
allowNull: false allowNull: false
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
Account: AccountModel Actor: ActorModel
@ForeignKey(() => VideoModel) @ForeignKey(() => VideoModel)
@Column @Column
@ -72,24 +72,24 @@ export class VideoShareModel extends Model<VideoShareModel> {
}) })
Video: VideoModel Video: VideoModel
static load (accountId: number, videoId: number, t: Sequelize.Transaction) { static load (actorId: number, videoId: number, t: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACCOUNT).findOne({ return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: { where: {
accountId, actorId,
videoId videoId
}, },
transaction: t transaction: t
}) })
} }
static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) { static loadActorsByShare (videoId: number, t: Sequelize.Transaction) {
const query = { const query = {
where: { where: {
videoId videoId
}, },
include: [ include: [
{ {
model: AccountModel, model: ActorModel,
required: true required: true
} }
], ],
@ -97,6 +97,6 @@ export class VideoShareModel extends Model<VideoShareModel> {
} }
return VideoShareModel.scope(ScopeNames.FULL).findAll(query) return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
.then(res => res.map(r => r.Account)) .then(res => res.map(r => r.Actor))
} }
} }

View File

@ -66,9 +66,10 @@ import {
VIDEO_PRIVACIES VIDEO_PRIVACIES
} from '../../initializers' } from '../../initializers'
import { getAnnounceActivityPubUrl } from '../../lib/activitypub' import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
import { sendDeleteVideo } from '../../lib/index' import { sendDeleteVideo } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate' import { AccountVideoRateModel } from '../account/account-video-rate'
import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { TagModel } from './tag' import { TagModel } from './tag'
@ -79,8 +80,7 @@ import { VideoShareModel } from './video-share'
import { VideoTagModel } from './video-tag' import { VideoTagModel } from './video-tag'
enum ScopeNames { enum ScopeNames {
NOT_IN_BLACKLIST = 'NOT_IN_BLACKLIST', AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
PUBLIC = 'PUBLIC',
WITH_ACCOUNT = 'WITH_ACCOUNT', WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_TAGS = 'WITH_TAGS', WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES', WITH_FILES = 'WITH_FILES',
@ -89,17 +89,13 @@ enum ScopeNames {
} }
@Scopes({ @Scopes({
[ScopeNames.NOT_IN_BLACKLIST]: { [ScopeNames.AVAILABLE_FOR_LIST]: {
where: { where: {
id: { id: {
[Sequelize.Op.notIn]: Sequelize.literal( [Sequelize.Op.notIn]: Sequelize.literal(
'(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
) )
} },
}
},
[ScopeNames.PUBLIC]: {
where: {
privacy: VideoPrivacy.PUBLIC privacy: VideoPrivacy.PUBLIC
} }
}, },
@ -114,8 +110,14 @@ enum ScopeNames {
required: true, required: true,
include: [ include: [
{ {
model: () => ServerModel, model: () => ActorModel,
required: false required: true,
include: [
{
model: () => ServerModel,
required: false
}
]
} }
] ]
} }
@ -138,7 +140,7 @@ enum ScopeNames {
include: [ include: [
{ {
model: () => VideoShareModel, model: () => VideoShareModel,
include: [ () => AccountModel ] include: [ () => ActorModel ]
} }
] ]
}, },
@ -271,7 +273,7 @@ export class VideoModel extends Model<VideoModel> {
@BelongsTo(() => VideoChannelModel, { @BelongsTo(() => VideoChannelModel, {
foreignKey: { foreignKey: {
allowNull: false allowNull: true
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
@ -351,14 +353,15 @@ export class VideoModel extends Model<VideoModel> {
return VideoModel.scope(ScopeNames.WITH_FILES).findAll() return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
} }
static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) { static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
function getRawQuery (select: string) { function getRawQuery (select: string) {
const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
'WHERE "VideoChannel"."accountId" = ' + accountId 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
'WHERE "Account"."actorId" = ' + actorId
const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' + const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
'WHERE "VideoShare"."accountId" = ' + accountId 'WHERE "VideoShare"."actorId" = ' + actorId
return `(${queryVideo}) UNION (${queryVideoShare})` return `(${queryVideo}) UNION (${queryVideoShare})`
} }
@ -388,11 +391,16 @@ export class VideoModel extends Model<VideoModel> {
} }
}, },
{ {
accountId actorId
} }
] ]
}, },
include: [ AccountModel ] include: [
{
model: ActorModel,
required: true
}
]
}, },
{ {
model: VideoChannelModel, model: VideoChannelModel,
@ -469,7 +477,7 @@ export class VideoModel extends Model<VideoModel> {
order: [ getSort(sort) ] order: [ getSort(sort) ]
} }
return VideoModel.scope([ ScopeNames.NOT_IN_BLACKLIST, ScopeNames.PUBLIC, ScopeNames.WITH_ACCOUNT ]) return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT ])
.findAndCountAll(query) .findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { return {
@ -541,7 +549,13 @@ export class VideoModel extends Model<VideoModel> {
const accountInclude: IIncludeOptions = { const accountInclude: IIncludeOptions = {
model: AccountModel, model: AccountModel,
include: [ serverInclude ] include: [
{
model: ActorModel,
required: true,
include: [ serverInclude ]
}
]
} }
const videoChannelInclude: IIncludeOptions = { const videoChannelInclude: IIncludeOptions = {
@ -586,7 +600,7 @@ export class VideoModel extends Model<VideoModel> {
videoChannelInclude, tagInclude videoChannelInclude, tagInclude
] ]
return VideoModel.scope([ ScopeNames.NOT_IN_BLACKLIST, ScopeNames.PUBLIC ]) return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST ])
.findAndCountAll(query).then(({ rows, count }) => { .findAndCountAll(query).then(({ rows, count }) => {
return { return {
data: rows, data: rows,
@ -688,8 +702,8 @@ export class VideoModel extends Model<VideoModel> {
toFormattedJSON () { toFormattedJSON () {
let serverHost let serverHost
if (this.VideoChannel.Account.Server) { if (this.VideoChannel.Account.Actor.Server) {
serverHost = this.VideoChannel.Account.Server.host serverHost = this.VideoChannel.Account.Actor.Server.host
} else { } else {
// It means it's our video // It means it's our video
serverHost = CONFIG.WEBSERVER.HOST serverHost = CONFIG.WEBSERVER.HOST
@ -805,9 +819,9 @@ export class VideoModel extends Model<VideoModel> {
for (const rate of this.AccountVideoRates) { for (const rate of this.AccountVideoRates) {
if (rate.type === 'like') { if (rate.type === 'like') {
likes.push(rate.Account.url) likes.push(rate.Account.Actor.url)
} else if (rate.type === 'dislike') { } else if (rate.type === 'dislike') {
dislikes.push(rate.Account.url) dislikes.push(rate.Account.Actor.url)
} }
} }
@ -820,7 +834,7 @@ export class VideoModel extends Model<VideoModel> {
const shares: string[] = [] const shares: string[] = []
for (const videoShare of this.VideoShares) { for (const videoShare of this.VideoShares) {
const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Actor)
shares.push(shareUrl) shares.push(shareUrl)
} }
@ -886,7 +900,13 @@ export class VideoModel extends Model<VideoModel> {
url, url,
likes: likesObject, likes: likesObject,
dislikes: dislikesObject, dislikes: dislikesObject,
shares: sharesObject shares: sharesObject,
attributedTo: [
{
type: 'Group',
id: this.VideoChannel.Actor.url
}
]
} }
} }
@ -1030,8 +1050,8 @@ export class VideoModel extends Model<VideoModel> {
baseUrlHttp = CONFIG.WEBSERVER.URL baseUrlHttp = CONFIG.WEBSERVER.URL
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
} else { } else {
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
} }
return { baseUrlHttp, baseUrlWs } return { baseUrlHttp, baseUrlWs }

View File

@ -184,25 +184,9 @@ describe('Test server follows API validators', function () {
.expect(403) .expect(403)
}) })
it('Should fail with an undefined id', async function () {
await request(server.url)
.delete(path + '/' + undefined)
.set('Authorization', 'Bearer ' + server.accessToken)
.set('Accept', 'application/json')
.expect(400)
})
it('Should fail with an invalid id', async function () {
await request(server.url)
.delete(path + '/foobar')
.set('Authorization', 'Bearer ' + server.accessToken)
.set('Accept', 'application/json')
.expect(400)
})
it('Should fail we do not follow this server', async function () { it('Should fail we do not follow this server', async function () {
await request(server.url) await request(server.url)
.delete(path + '/-1') .delete(path + '/example.com')
.set('Authorization', 'Bearer ' + server.accessToken) .set('Authorization', 'Bearer ' + server.accessToken)
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(404) .expect(404)

View File

@ -23,7 +23,6 @@ const expect = chai.expect
describe('Test follows', function () { describe('Test follows', function () {
let servers: ServerInfo[] = [] let servers: ServerInfo[] = []
let server3Id: number
before(async function () { before(async function () {
this.timeout(20000) this.timeout(20000)
@ -82,8 +81,6 @@ describe('Test follows', function () {
expect(server3Follow).to.not.be.undefined expect(server3Follow).to.not.be.undefined
expect(server2Follow.state).to.equal('accepted') expect(server2Follow.state).to.equal('accepted')
expect(server3Follow.state).to.equal('accepted') expect(server3Follow.state).to.equal('accepted')
server3Id = server3Follow.following.id
}) })
it('Should have 0 followings on server 1 and 2', async function () { it('Should have 0 followings on server 1 and 2', async function () {
@ -121,7 +118,7 @@ describe('Test follows', function () {
it('Should unfollow server 3 on server 1', async function () { it('Should unfollow server 3 on server 1', async function () {
this.timeout(5000) this.timeout(5000)
await unfollow(servers[0].url, servers[0].accessToken, server3Id) await unfollow(servers[0].url, servers[0].accessToken, servers[2])
await wait(3000) await wait(3000)
}) })

View File

@ -42,8 +42,8 @@ async function follow (follower: string, following: string[], accessToken: strin
return res return res
} }
async function unfollow (url: string, accessToken: string, id: number, expectedStatus = 204) { async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
const path = '/api/v1/server/following/' + id const path = '/api/v1/server/following/' + target.host
const res = await request(url) const res = await request(url)
.delete(path) .delete(path)

View File

@ -1,14 +1,14 @@
import { ActivityPubSignature } from './activitypub-signature' import { ActivityPubSignature } from './activitypub-signature'
import { VideoChannelObject, VideoTorrentObject } from './objects' import { VideoTorrentObject } from './objects'
import { DislikeObject } from './objects/dislike-object' import { DislikeObject } from './objects/dislike-object'
import { VideoAbuseObject } from './objects/video-abuse-object' import { VideoAbuseObject } from './objects/video-abuse-object'
import { ViewObject } from './objects/view-object' import { ViewObject } from './objects/view-object'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | export type Activity = ActivityCreate | ActivityUpdate |
ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
ActivityUndo | ActivityLike ActivityUndo | ActivityLike
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like'
export interface ActivityAudience { export interface ActivityAudience {
to: string[] to: string[]
@ -27,18 +27,12 @@ export interface BaseActivity {
export interface ActivityCreate extends BaseActivity { export interface ActivityCreate extends BaseActivity {
type: 'Create' type: 'Create'
object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject
}
export interface ActivityAdd extends BaseActivity {
type: 'Add'
target: string
object: VideoTorrentObject
} }
export interface ActivityUpdate extends BaseActivity { export interface ActivityUpdate extends BaseActivity {
type: 'Update' type: 'Update'
object: VideoTorrentObject | VideoChannelObject object: VideoTorrentObject
} }
export interface ActivityDelete extends BaseActivity { export interface ActivityDelete extends BaseActivity {
@ -56,7 +50,7 @@ export interface ActivityAccept extends BaseActivity {
export interface ActivityAnnounce extends BaseActivity { export interface ActivityAnnounce extends BaseActivity {
type: 'Announce' type: 'Announce'
object: ActivityCreate | ActivityAdd object: ActivityCreate
} }
export interface ActivityUndo extends BaseActivity { export interface ActivityUndo extends BaseActivity {

View File

@ -1,6 +1,10 @@
import { ActivityPubAttributedTo } from './objects/common-objects'
export type ActivityPubActorType = 'Person' | 'Application' | 'Group'
export interface ActivityPubActor { export interface ActivityPubActor {
'@context': any[] '@context': any[]
type: 'Person' | 'Application' | 'Group' type: ActivityPubActorType
id: string id: string
following: string following: string
followers: string followers: string
@ -12,6 +16,8 @@ export interface ActivityPubActor {
endpoints: { endpoints: {
sharedInbox: string sharedInbox: string
} }
summary: string
attributedTo: ActivityPubAttributedTo[]
uuid: string uuid: string
publicKey: { publicKey: {
@ -21,7 +27,6 @@ export interface ActivityPubActor {
} }
// Not used // Not used
// summary: string
// icon: string[] // icon: string[]
// liked: string // liked: string
} }

View File

@ -23,3 +23,8 @@ export interface ActivityUrlObject {
width: number width: number
size?: number size?: number
} }
export interface ActivityPubAttributedTo {
type: 'Group' | 'Person'
id: string
}

View File

@ -1,6 +1,5 @@
export * from './common-objects' export * from './common-objects'
export * from './video-abuse-object' export * from './video-abuse-object'
export * from './video-channel-object'
export * from './video-torrent-object' export * from './video-torrent-object'
export * from './view-object' export * from './view-object'
export * from './dislike-object' export * from './dislike-object'

View File

@ -1,13 +0,0 @@
import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
export interface VideoChannelObject {
type: 'VideoChannel'
id: string
name: string
content: string
uuid: string
published: string
updated: string
actor?: string
shares?: ActivityPubOrderedCollection<string>
}

View File

@ -1,6 +1,6 @@
import { import {
ActivityIconObject, ActivityIconObject,
ActivityIdentifierObject, ActivityIdentifierObject, ActivityPubAttributedTo,
ActivityTagObject, ActivityTagObject,
ActivityUrlObject ActivityUrlObject
} from './common-objects' } from './common-objects'
@ -24,8 +24,8 @@ export interface VideoTorrentObject {
content: string content: string
icon: ActivityIconObject icon: ActivityIconObject
url: ActivityUrlObject[] url: ActivityUrlObject[]
actor?: string
likes?: ActivityPubOrderedCollection<string> likes?: ActivityPubOrderedCollection<string>
dislikes?: ActivityPubOrderedCollection<string> dislikes?: ActivityPubOrderedCollection<string>
shares?: ActivityPubOrderedCollection<string> shares?: ActivityPubOrderedCollection<string>
attributedTo: ActivityPubAttributedTo[]
} }

View File

@ -1,4 +1,4 @@
export * from './accounts' export * from './actors'
export * from './activitypub' export * from './activitypub'
export * from './users' export * from './users'
export * from './videos' export * from './videos'

View File

@ -1,4 +1,4 @@
import { Account } from '../accounts' import { Account } from '../actors'
import { VideoChannel } from '../videos/video-channel.model' import { VideoChannel } from '../videos/video-channel.model'
import { UserRole } from './user-role' import { UserRole } from './user-role'

View File

@ -1,4 +1,4 @@
import { Account } from '../accounts' import { Account } from '../actors'
import { VideoChannel } from './video-channel.model' import { VideoChannel } from './video-channel.model'
import { VideoPrivacy } from './video-privacy.enum' import { VideoPrivacy } from './video-privacy.enum'