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 { 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 { FollowService } from '../shared'

View File

@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
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 { RestPagination, RestTable } from '../../../shared'
import { FollowService } from '../shared'

View File

@ -10,7 +10,7 @@ import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { Subject } from 'rxjs/Subject'
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 { environment } from '../../../environments/environment'
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 { 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 { AuthUser } from '../../core'
import {

View File

@ -1,6 +1,6 @@
import { User } from '../'
import { Video as VideoServerModel } from '../../../../../shared'
import { Account } from '../../../../../shared/models/accounts'
import { Account } from '../../../../../shared/models/actors'
import { environment } from '../../../environments/environment'
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 { AccountFollowModel } from '../server/models/account/account-follow'
import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
import { VideoModel } from '../server/models/video/video'
initDatabaseModels(true)
.then(() => {
return getServerAccount()
return getServerActor()
})
.then(serverAccount => {
return AccountFollowModel.listAcceptedFollowingUrlsForApi([ serverAccount.id ], undefined)
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ serverAccount.id ], undefined)
})
.then(res => {
return res.total > 0

View File

@ -50,7 +50,8 @@ migrate()
// ----------- PeerTube modules -----------
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'
// ----------- Command line -----------

View File

@ -2,20 +2,13 @@
import * as express from 'express'
import { activityPubCollectionPagination, pageToStartAndCount } from '../../helpers'
import { ACTIVITY_PUB, CONFIG } from '../../initializers'
import { buildVideoChannelAnnounceToFollowers } from '../../lib/activitypub/send'
import { buildVideoAnnounceToFollowers } from '../../lib/index'
import { buildVideoAnnounceToFollowers } from '../../lib/activitypub/send'
import { asyncMiddleware, executeIfActivityPub, localAccountValidator } from '../../middlewares'
import {
videoChannelsGetValidator,
videoChannelsShareValidator,
videosGetValidator,
videosShareValidator
} from '../../middlewares/validators'
import { videoChannelsGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
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 { VideoChannelModel } from '../../models/video/video-channel'
import { VideoChannelShareModel } from '../../models/video/video-channel-share'
import { VideoShareModel } from '../../models/video/video-share'
const activityPubClientRouter = express.Router()
@ -50,11 +43,6 @@ activityPubClientRouter.get('/video-channels/:id',
executeIfActivityPub(asyncMiddleware(videoChannelController))
)
activityPubClientRouter.get('/video-channels/:id/announces/:accountId',
executeIfActivityPub(asyncMiddleware(videoChannelsShareValidator)),
executeIfActivityPub(asyncMiddleware(videoChannelAnnounceController))
)
// ---------------------------------------------------------------------------
export {
@ -75,7 +63,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
const page = req.query.page || 1
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)
return res.json(activityPubResult)
@ -87,7 +75,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
const page = req.query.page || 1
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)
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) {
const share = res.locals.videoShare as VideoShareModel
const object = await buildVideoAnnounceToFollowers(share.Account, 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)
const object = await buildVideoAnnounceToFollowers(share.Actor, res.locals.video, undefined)
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 { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
import { ActorModel } from '../../models/activitypub/actor'
const inboxRouter = express.Router()
@ -48,7 +49,14 @@ async function inboxController (req: express.Request, res: express.Response, nex
activities = activities.filter(a => isActivityValid(a))
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()
}

View File

@ -3,9 +3,8 @@ import { Activity } from '../../../shared/models/activitypub/activity'
import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { pageToStartAndCount } from '../../helpers/core-utils'
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 { announceActivityData } from '../../lib/index'
import { asyncMiddleware, localAccountValidator } from '../../middlewares'
import { AccountModel } from '../../models/account/account'
import { VideoModel } from '../../models/video/video'
@ -27,29 +26,30 @@ export {
async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
const account: AccountModel = res.locals.account
const actor = account.Actor
const page = req.query.page || 1
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[] = []
for (const video of data.data) {
const videoObject = video.toActivityPubObject()
// This is a shared video
const videoChannel = video.VideoChannel
// This is a shared video
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 announceActivity = await announceActivityData(url, account, addActivity, undefined)
const url = getAnnounceActivityPubUrl(video.url, actor)
const announceActivity = await announceActivityData(url, actor, createActivity, undefined)
activities.push(announceActivity)
} 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,
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()
}

View File

@ -1,10 +1,9 @@
import * as express from 'express'
import { UserRight } from '../../../../shared/models/users'
import { getAccountFromWebfinger, getFormattedObjects, getServerAccount, logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript, SERVER_ACCOUNT_NAME } from '../../../initializers'
import { saveAccountAndServerIfNotExist } from '../../../lib/activitypub'
import { sendUndoFollow } from '../../../lib/activitypub/send'
import { sendFollow } from '../../../lib/index'
import { getFormattedObjects, getServerActor, loadActorUrlOrGetFromWebfinger, logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
import { getOrCreateActorAndServerAndModel } from '../../../lib/activitypub'
import { sendFollow, sendUndoFollow } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
authenticate,
@ -17,8 +16,8 @@ import {
setPagination
} from '../../../middlewares'
import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators'
import { AccountModel } from '../../../models/account/account'
import { AccountFollowModel } from '../../../models/account/account-follow'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
const serverFollowsRouter = express.Router()
@ -38,7 +37,7 @@ serverFollowsRouter.post('/following',
asyncMiddleware(followRetry)
)
serverFollowsRouter.delete('/following/:accountId',
serverFollowsRouter.delete('/following/:host',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(removeFollowingValidator),
@ -62,43 +61,41 @@ export {
// ---------------------------------------------------------------------------
async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
const serverAccount = await getServerAccount()
const resultList = await AccountFollowModel.listFollowingForApi(serverAccount.id, req.query.start, req.query.count, req.query.sort)
const serverActor = await getServerActor()
const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
const serverAccount = await getServerAccount()
const resultList = await AccountFollowModel.listFollowersForApi(serverAccount.id, req.query.start, req.query.count, req.query.sort)
const serverActor = await getServerActor()
const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function followRetry (req: express.Request, res: express.Response, next: express.NextFunction) {
const hosts = req.body.hosts as string[]
const fromAccount = await getServerAccount()
const fromActor = await getServerActor()
const tasks: Promise<any>[] = []
const accountName = SERVER_ACCOUNT_NAME
const actorName = SERVER_ACTOR_NAME
for (const host of hosts) {
// We process each host in a specific transaction
// First, we add the follow request in the database
// Then we send the follow request to other account
const p = loadLocalOrGetAccountFromWebfinger(accountName, host)
.then(accountResult => {
let targetAccount = accountResult.account
// Then we send the follow request to other actor
const p = loadActorUrlOrGetFromWebfinger(actorName, host)
.then(actorUrl => getOrCreateActorAndServerAndModel(actorUrl))
.then(targetActor => {
const options = {
arguments: [ fromAccount, targetAccount, accountResult.loadedFromDB ],
arguments: [ fromActor, targetActor ],
errorMessage: 'Cannot follow with many retries.'
}
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)
}
@ -110,42 +107,32 @@ async function followRetry (req: express.Request, res: express.Response, next: e
return res.status(204).end()
}
async function follow (fromAccount: AccountModel, targetAccount: AccountModel, targetAlreadyInDB: boolean) {
try {
await sequelizeTypescript.transaction(async t => {
if (targetAlreadyInDB === false) {
await saveAccountAndServerIfNotExist(targetAccount, t)
}
const [ accountFollow ] = await AccountFollowModel.findOrCreate({
where: {
accountId: fromAccount.id,
targetAccountId: targetAccount.id
},
defaults: {
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)
}
function follow (fromActor: ActorModel, targetActor: ActorModel) {
return sequelizeTypescript.transaction(async t => {
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
where: {
actorId: fromActor.id,
targetActorId: targetActor.id
},
defaults: {
state: 'pending',
actorId: fromActor.id,
targetActorId: targetActor.id
},
transaction: t
})
} catch (err) {
// Reset target account
targetAccount.isNewRecord = !targetAlreadyInDB
throw err
}
actorFollow.ActorFollowing = targetActor
actorFollow.ActorFollower = fromActor
// 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) {
const follow: AccountFollowModel = res.locals.follow
const follow: ActorFollowModel = res.locals.follow
await sequelizeTypescript.transaction(async 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 })
})
// 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
const following = follow.AccountFollowing
const following = follow.ActorFollowing
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()
}
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 { getFormattedObjects, logger, retryTransactionWrapper } from '../../helpers'
import { CONFIG } from '../../initializers'
import { createUserAccountAndChannel } from '../../lib'
import { createUserAccountAndChannel } from '../../lib/user'
import {
asyncMiddleware,
authenticate,

View File

@ -1,22 +1,18 @@
import * as express from 'express'
import {
logger,
getFormattedObjects,
retryTransactionWrapper
} from '../../../helpers'
import { UserRight, VideoAbuseCreate } from '../../../../shared'
import { getFormattedObjects, logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { sendVideoAbuse } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
videoAbuseReportValidator,
videoAbusesSortValidator,
setVideoAbusesSort,
setPagination,
asyncMiddleware
setVideoAbusesSort,
videoAbuseReportValidator,
videoAbusesSortValidator
} from '../../../middlewares'
import { VideoAbuseCreate, UserRight } from '../../../../shared'
import { sendVideoAbuse } from '../../../lib/index'
import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video'
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
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 { getFormattedObjects, logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { createVideoChannel } from '../../../lib'
import { sendUpdateVideoChannel } from '../../../lib/activitypub/send/send-update'
import { setAsyncActorKeys } from '../../../lib/activitypub'
import { createVideoChannel } from '../../../lib/video-channel'
import {
asyncMiddleware,
authenticate,
@ -92,15 +92,17 @@ async function addVideoChannelRetryWrapper (req: express.Request, res: express.R
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 account: AccountModel = res.locals.oauth.token.User.Account
return sequelizeTypescript.transaction(async t => {
const videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t)
logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid)
const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
return createVideoChannel(videoChannelInfo, account, t)
})
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) {
@ -128,12 +130,13 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
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) {
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) {
const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
await sequelizeTypescript.transaction(async t => {
return sequelizeTypescript.transaction(async 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) {

View File

@ -11,7 +11,7 @@ import {
resetSequelizeInstance,
retryTransactionWrapper
} from '../../../helpers'
import { getServerAccount } from '../../../helpers/utils'
import { getServerActor } from '../../../helpers/utils'
import {
CONFIG,
sequelizeTypescript,
@ -22,8 +22,7 @@ import {
VIDEO_PRIVACIES
} from '../../../initializers'
import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServer } from '../../../lib/activitypub'
import { sendAddVideo, sendCreateViewToOrigin, sendUpdateVideo } from '../../../lib/activitypub/send'
import { sendCreateViewToVideoFollowers } from '../../../lib/index'
import { sendCreateVideo, sendCreateViewToOrigin, sendCreateViewToVideoFollowers, sendUpdateVideo } from '../../../lib/activitypub/send'
import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler'
import {
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
if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
await sendAddVideo(video, t)
await sendCreateVideo(video, t)
// TODO: share by video channel
await shareVideoByServer(video, t)
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
if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
await sendAddVideo(videoInstanceUpdated, t)
await sendCreateVideo(videoInstanceUpdated, t)
// TODO: Send by video channel
await shareVideoByServer(videoInstanceUpdated, t)
}
})
@ -330,7 +331,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video
await videoInstance.increment('views')
const serverAccount = await getServerAccount()
const serverAccount = await getServerActor()
if (videoInstance.isOwned()) {
await sendCreateViewToVideoFollowers(serverAccount, videoInstance, undefined)

View File

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

View File

@ -1,7 +1,7 @@
import * as express from 'express'
import { asyncMiddleware } from '../middlewares'
import { webfingerValidator } from '../middlewares/validators'
import { AccountModel } from '../models/account/account'
import { ActorModel } from '../models/activitypub/actor'
const webfingerRouter = express.Router()
@ -19,16 +19,16 @@ export {
// ---------------------------------------------------------------------------
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 = {
subject: req.query.resource,
aliases: [ account.Actor.url ],
aliases: [ actor.url ],
links: [
{
rel: 'self',
type: 'application/activity+json',
href: account.Actor.url
href: actor.url
}
]
}

View File

@ -1,7 +1,7 @@
import { ResultList } from '../../shared/models'
import { Activity } from '../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../initializers'
import { AccountModel } from '../models/account/account'
import { ActorModel } from '../models/activitypub/actor'
import { signObject } from './peertube-crypto'
function activityPubContextify <T> (data: T) {
@ -71,10 +71,10 @@ function activityPubCollectionPagination (url: string, page: any, result: Result
return orderedCollectionPagination
}
function buildSignedActivity (byAccount: AccountModel, data: Object) {
function buildSignedActivity (byActor: ActorModel, data: Object) {
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 { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './actor'
import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid } from './actor'
import { isAnnounceActivityValid } from './announce'
import { isActivityPubUrlValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate'
import { isUndoActivityValid } from './undo'
import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
import {
isVideoFlagValid,
isVideoTorrentAddActivityValid,
isVideoTorrentCreateActivityValid,
isVideoTorrentDeleteActivityValid,
isVideoTorrentUpdateActivityValid
} from './videos'
@ -29,7 +29,6 @@ function isRootActivityValid (activity: any) {
const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
Create: checkCreateActivity,
Add: checkAddActivity,
Update: checkUpdateActivity,
Delete: checkDeleteActivity,
Follow: checkFollowActivity,
@ -59,14 +58,10 @@ export {
function checkCreateActivity (activity: any) {
return isViewActivityValid(activity) ||
isDislikeActivityValid(activity) ||
isVideoChannelCreateActivityValid(activity) ||
isVideoTorrentCreateActivityValid(activity) ||
isVideoFlagValid(activity)
}
function checkAddActivity (activity: any) {
return isVideoTorrentAddActivityValid(activity)
}
function checkUpdateActivity (activity: any) {
return isVideoTorrentUpdateActivityValid(activity) ||
isVideoChannelUpdateActivityValid(activity)
@ -75,15 +70,15 @@ function checkUpdateActivity (activity: any) {
function checkDeleteActivity (activity: any) {
return isVideoTorrentDeleteActivityValid(activity) ||
isVideoChannelDeleteActivityValid(activity) ||
isAccountDeleteActivityValid(activity)
isActorDeleteActivityValid(activity)
}
function checkFollowActivity (activity: any) {
return isAccountFollowActivityValid(activity)
return isActorFollowActivityValid(activity)
}
function checkAcceptActivity (activity: any) {
return isAccountAcceptActivityValid(activity)
return isActorAcceptActivityValid(activity)
}
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 { CONSTRAINTS_FIELDS } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
import { isAccountNameValid } from '../accounts'
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) {
return isActivityPubUrlValid(endpointObject.sharedInbox)
@ -27,7 +31,12 @@ function isActorPublicKeyValid (publicKey: 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) {
@ -46,10 +55,16 @@ function isRemoteActorValid (remoteActor: any) {
isActivityPubUrlValid(remoteActor.followers) &&
isActivityPubUrlValid(remoteActor.inbox) &&
isActivityPubUrlValid(remoteActor.outbox) &&
isActorNameValid(remoteActor.name) &&
isActorPreferredUsernameValid(remoteActor.preferredUsername) &&
isActivityPubUrlValid(remoteActor.url) &&
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) {
@ -73,6 +88,40 @@ function isActorAcceptActivityValid (activity: any) {
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 {
@ -87,5 +136,9 @@ export {
isActorFollowersCountValid,
isActorFollowActivityValid,
isActorAcceptActivityValid,
isActorDeleteActivityValid
isActorDeleteActivityValid,
isActorIdExist,
isLocalActorNameExist,
isActorNameValid,
isActorExist
}

View File

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

View File

@ -17,7 +17,7 @@ function isActivityPubUrlValid (url: string) {
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) {
@ -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 {
isActivityPubUrlValid,
isBaseActivityValid
isBaseActivityValid,
setValidAttributedTo
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { jsig } from './custom-jsonld-signature'
import { logger } from './logger'
@ -13,18 +13,18 @@ async function createPrivateAndPublicKeys () {
return { privateKey: key, publicKey }
}
function isSignatureVerified (fromAccount: AccountModel, signedDocument: object) {
function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
const publicKeyObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromAccount.url,
'@id': fromActor.url,
'@type': 'CryptographicKey',
owner: fromAccount.url,
publicKeyPem: fromAccount.publicKey
owner: fromActor.url,
publicKeyPem: fromActor.publicKey
}
const publicKeyOwnerObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromAccount.url,
'@id': fromActor.url,
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 = {
privateKeyPem: byAccount.privateKey,
creator: byAccount.url
privateKeyPem: byActor.privateKey,
creator: byActor.url
}
return jsig.promises.sign(data, options)

View File

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

View File

@ -1,6 +1,6 @@
import * as WebFinger from 'webfinger.js'
import { WebFingerData } from '../../shared'
import { fetchRemoteAccount } from '../lib/activitypub'
import { ActorModel } from '../models/activitypub/actor'
import { isTestInstance } from './core-utils'
import { isActivityPubUrlValid } from './custom-validators/activitypub'
@ -11,9 +11,23 @@ const webfinger = new WebFinger({
request_timeout: 3000
})
async function getAccountFromWebfinger (nameWithHost: string) {
const webfingerData: WebFingerData = await webfingerLookup(nameWithHost)
async function loadActorUrlOrGetFromWebfinger (name: string, host: string) {
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.')
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.')
}
const account = await fetchRemoteAccount(selfLink.href)
if (account === undefined) throw new Error('Cannot fetch remote account ' + selfLink.href)
return account
return selfLink.href
}
// ---------------------------------------------------------------------------
export {
getAccountFromWebfinger
}
// ---------------------------------------------------------------------------
function webfingerLookup (nameWithHost: string) {
return new Promise<WebFingerData>((res, rej) => {
webfinger.lookup(nameWithHost, (err, p) => {

View File

@ -1,7 +1,8 @@
import * as config from 'config'
import { join } from 'path'
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'
// Do not use barrels, remain constants as independent as possible
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 = {
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
@ -350,12 +357,13 @@ export {
REMOTE_SCHEME,
FOLLOW_STATES,
AVATARS_DIR,
SERVER_ACCOUNT_NAME,
SERVER_ACTOR_NAME,
PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,
STATIC_MAX_AGE,
STATIC_PATHS,
ACTIVITY_PUB,
ACTIVITY_PUB_ACTOR_TYPES,
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,

View File

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

View File

@ -1,12 +1,12 @@
import * as passwordGenerator from 'password-generator'
import { UserRole } from '../../shared'
import { createPrivateAndPublicKeys, logger, mkdirpPromise, rimrafPromise } from '../helpers'
import { createLocalAccountWithoutKeys, createUserAccountAndChannel } from '../lib'
import { logger, mkdirpPromise, rimrafPromise } from '../helpers'
import { createApplicationActor, createUserAccountAndChannel } from '../lib/user'
import { UserModel } from '../models/account/user'
import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
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'
async function installApplication () {
@ -134,15 +134,12 @@ async function createApplicationIfNotExist () {
if (exist === true) return undefined
logger.info('Creating Application table.')
const applicationInstance = await ApplicationModel.create({ migrationVersion: LAST_MIGRATION_VERSION })
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()
accountCreated.set('publicKey', publicKey)
accountCreated.set('privateKey', privateKey)
return accountCreated.save()
return createApplicationActor(application.id)
}

View File

@ -5,7 +5,7 @@ import { shareVideoByServer } from '../../lib/activitypub/share'
import { getVideoActivityPubUrl, getVideoChannelActivityPubUrl } from '../../lib/activitypub/url'
import { createLocalAccountWithoutKeys } from '../../lib/user'
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: {
transaction: Sequelize.Transaction,
@ -66,7 +66,7 @@ async function up (utils: {
// Create application account
{
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()
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 { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
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 = {
uris: [ account.outboxUrl ]
uris: [ actor.outboxUrl ]
}
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpFetcherHandler', jobPayload)

View File

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

View File

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

View File

@ -1,29 +1,13 @@
import * as magnetUtil from 'magnet-uri'
import { VideoTorrentObject } from '../../../../shared'
import { VideoChannelObject } from '../../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { doRequest } from '../../../helpers'
import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos'
import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
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 { getOrCreateAccountAndServer } from '../account'
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
}
}
import { getOrCreateActorAndServerAndModel } from '../actor'
async function videoActivityObjectToDBAttributes (
videoChannel: VideoChannelModel,
@ -120,13 +104,13 @@ async function addVideoShares (instance: VideoModel, shares: string[]) {
uri: share,
json: true
})
const actor = json['actor']
if (!actor) continue
const actorUrl = json['actor']
if (!actorUrl) continue
const account = await getOrCreateAccountAndServer(actor)
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = {
accountId: account.id,
actorId: actor.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 {
videoFileActivityUrlToDBAttributes,
videoActivityObjectToDBAttributes,
videoChannelActivityObjectToDBAttributes,
addVideoChannelShares,
addVideoShares
}

View File

@ -1,14 +1,14 @@
import { ActivityAccept } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account'
import { AccountFollowModel } from '../../../models/account/account-follow'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { addFetchOutboxJob } from '../fetch'
async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountModel) {
if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
async function processAcceptActivity (activity: ActivityAccept, inboxActor?: ActorModel) {
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) {
const follow = await AccountFollowModel.loadByAccountAndTarget(account.id, targetAccount.id)
async function processAccept (actor: ActorModel, targetActor: ActorModel) {
const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id)
if (!follow) throw new Error('Cannot find associated follow.')
follow.set('state', 'accepted')
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 { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
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 { getOrCreateAccountAndServer } from '../account'
import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc'
import { processAddActivity } from './process-add'
import { processCreateActivity } from './process-create'
async function processAnnounceActivity (activity: ActivityAnnounce) {
const announcedActivity = activity.object
const accountAnnouncer = await getOrCreateAccountAndServer(activity.actor)
const actorAnnouncer = await getOrCreateActorAndServerAndModel(activity.actor)
if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
return processVideoChannelShare(accountAnnouncer, activity)
} else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
return processVideoShare(accountAnnouncer, activity)
if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'Video') {
return processVideoShare(actorAnnouncer, activity)
}
logger.warn(
@ -37,60 +32,24 @@ export {
// ---------------------------------------------------------------------------
function processVideoChannelShare (accountAnnouncer: AccountModel, activity: ActivityAnnounce) {
function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
const options = {
arguments: [ accountAnnouncer, 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 ],
arguments: [ actorAnnouncer, activity ],
errorMessage: 'Cannot share the video with many retries.'
}
return retryTransactionWrapper(shareVideo, options)
}
function shareVideo (accountAnnouncer: AccountModel, activity: ActivityAnnounce) {
const announcedActivity = activity.object as ActivityAdd
function shareVideo (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
const announcedActivity = activity.object
return sequelizeTypescript.transaction(async t => {
// Add share entry
const video: VideoModel = await processAddActivity(announcedActivity)
const video: VideoModel = await processCreateActivity(announcedActivity)
const share = {
accountId: accountAnnouncer.id,
actorId: actorAnnouncer.id,
videoId: video.id
}
@ -102,7 +61,7 @@ function shareVideo (accountAnnouncer: AccountModel, activity: ActivityAnnounce)
if (video.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ accountAnnouncer ]
const exceptions = [ actorAnnouncer ]
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 { 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 { ActorModel } from '../../../models/activitypub/actor'
import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video'
import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { getOrCreateAccountAndServer } from '../account'
import { VideoFileModel } from '../../../models/video/video-file'
import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc'
import { getVideoChannelActivityPubUrl } from '../url'
import { addVideoChannelShares, videoChannelActivityObjectToDBAttributes } from './misc'
import { generateThumbnailFromUrl } from '../videos'
import { addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object
const activityType = activityObject.type
const account = await getOrCreateAccountAndServer(activity.actor)
const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (activityType === 'View') {
return processCreateView(account, activity)
return processCreateView(actor, activity)
} else if (activityType === 'Dislike') {
return processCreateDislike(account, activity)
} else if (activityType === 'VideoChannel') {
return processCreateVideoChannel(account, activityObject as VideoChannelObject)
return processCreateDislike(actor, activity)
} else if (activityType === 'Video') {
return processCreateVideo(actor, activity)
} 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 })
@ -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 = {
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.'
}
return retryTransactionWrapper(createVideoDislike, options)
}
function createVideoDislike (byAccount: AccountModel, activity: ActivityCreate) {
function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) {
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 => {
const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t)
@ -69,20 +178,20 @@ function createVideoDislike (byAccount: AccountModel, activity: ActivityCreate)
if (video.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
const exceptions = [ byActor ]
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 video = await VideoModel.loadByUrlAndPopulateAccount(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)
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 = {
arguments: [ account, videoChannelToCreateData ],
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 ],
arguments: [ actor, videoAbuseToCreateData ],
errorMessage: 'Cannot insert the remote video abuse with many retries.'
}
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)
const account = actor.Account
if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url)
return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
if (!video) {

View File

@ -2,28 +2,30 @@ import { ActivityDelete } from '../../../../shared/models/activitypub'
import { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { getOrCreateAccountAndServer } from '../account'
import { getOrCreateActorAndServerAndModel } from '../actor'
async function processDeleteActivity (activity: ActivityDelete) {
const account = await getOrCreateAccountAndServer(activity.actor)
const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (account.url === activity.id) {
return processDeleteAccount(account)
if (actor.url === activity.id) {
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)
if (videoObject !== undefined) {
return processDeleteVideo(account, videoObject)
}
}
{
let videoChannelObject = await VideoChannelModel.loadByUrl(activity.id)
if (videoChannelObject !== undefined) {
return processDeleteVideoChannel(account, videoChannelObject)
return processDeleteVideo(actor, videoObject)
}
}
@ -38,21 +40,21 @@ export {
// ---------------------------------------------------------------------------
async function processDeleteVideo (account: AccountModel, videoToDelete: VideoModel) {
async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel) {
const options = {
arguments: [ account, videoToDelete ],
arguments: [ actor, videoToDelete ],
errorMessage: 'Cannot remove the remote video with many retries.'
}
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)
await sequelizeTypescript.transaction(async t => {
if (videoToDelete.VideoChannel.Account.id !== account.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
if (videoToDelete.VideoChannel.Account.Actor.id !== actor.id) {
throw new Error('Account ' + actor.url + ' does not own video channel ' + videoToDelete.VideoChannel.Actor.url)
}
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)
}
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) {
const options = {
arguments: [ accountToRemove ],
@ -94,11 +73,30 @@ async function processDeleteAccount (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 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 { logger, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { AccountFollowModel } from '../../../models/account/account-follow'
import { getOrCreateAccountAndServer } from '../account'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getOrCreateActorAndServerAndModel } from '../actor'
import { sendAccept } from '../send'
async function processFollowActivity (activity: ActivityFollow) {
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 = {
arguments: [ account, targetAccountURL ],
arguments: [ actor, targetActorURL ],
errorMessage: 'Cannot follow with many retries.'
}
return retryTransactionWrapper(follow, options)
}
async function follow (account: AccountModel, targetAccountURL: string) {
async function follow (actor: ActorModel, targetActorURL: string) {
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 (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
if (!targetActor) throw new Error('Unknown actor')
if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
const [ accountFollow ] = await AccountFollowModel.findOrCreate({
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
where: {
accountId: account.id,
targetAccountId: targetAccount.id
actorId: actor.id,
targetActorId: targetActor.id
},
defaults: {
accountId: account.id,
targetAccountId: targetAccount.id,
actorId: actor.id,
targetActorId: targetActor.id,
state: 'accepted'
},
transaction: t
})
if (accountFollow.state !== 'accepted') {
accountFollow.state = 'accepted'
await accountFollow.save({ transaction: t })
if (actorFollow.state !== 'accepted') {
actorFollow.state = 'accepted'
await actorFollow.save({ transaction: t })
}
accountFollow.AccountFollower = account
accountFollow.AccountFollowing = targetAccount
actorFollow.ActorFollower = actor
actorFollow.ActorFollowing = targetActor
// Target sends to account he accepted the follow request
return sendAccept(accountFollow, t)
// Target sends to actor he accepted the follow request
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 { retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video'
import { getOrCreateAccountAndServer } from '../account'
import { getOrCreateActorAndServerAndModel } from '../actor'
import { forwardActivity } from '../send/misc'
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 = {
arguments: [ byAccount, activity ],
arguments: [ actor, activity ],
errorMessage: 'Cannot like the video with many retries.'
}
return retryTransactionWrapper(createVideoLike, options)
}
function createVideoLike (byAccount: AccountModel, activity: ActivityLike) {
function createVideoLike (byActor: ActorModel, activity: ActivityLike) {
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 => {
const video = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
@ -52,7 +55,7 @@ function createVideoLike (byAccount: AccountModel, activity: ActivityLike) {
if (video.isOwned() && created === true) {
// Don't resend the activity to the sender
const exceptions = [ byAccount ]
const exceptions = [ byActor ]
await forwardActivity(activity, t, exceptions)
}
})

View File

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

View File

@ -1,23 +1,19 @@
import * as Bluebird from 'bluebird'
import { VideoChannelObject, VideoTorrentObject } from '../../../../shared'
import { ActivityUpdate } from '../../../../shared/models/activitypub'
import { logger, resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
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 { getOrCreateActorAndServerAndModel } from '../actor'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processUpdateActivity (activity: ActivityUpdate) {
const account = await getOrCreateAccountAndServer(activity.actor)
const actor = await getOrCreateActorAndServerAndModel(activity.actor)
if (activity.object.type === 'Video') {
return processUpdateVideo(account, activity.object)
} else if (activity.object.type === 'VideoChannel') {
return processUpdateVideoChannel(account, activity.object)
return processUpdateVideo(actor, activity)
}
return
@ -31,16 +27,18 @@ export {
// ---------------------------------------------------------------------------
function processUpdateVideo (account: AccountModel, video: VideoTorrentObject) {
function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
const options = {
arguments: [ account, video ],
arguments: [ actor, activity ],
errorMessage: 'Cannot update the remote video with many retries'
}
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)
let videoInstance: VideoModel
let videoFieldsSave: object
@ -54,23 +52,23 @@ async function updateRemoteVideo (account: AccountModel, videoAttributesToUpdate
const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
if (videoInstance.VideoChannel.Account.id !== account.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
const videoChannel = videoInstance.VideoChannel
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('category', videoData.category)
videoInstance.set('licence', videoData.licence)
videoInstance.set('language', videoData.language)
videoInstance.set('nsfw', videoData.nsfw)
videoInstance.set('privacy', videoData.privacy)
videoInstance.set('description', videoData.description)
videoInstance.set('duration', videoData.duration)
videoInstance.set('createdAt', videoData.createdAt)
videoInstance.set('updatedAt', videoData.updatedAt)
videoInstance.set('views', videoData.views)
// videoInstance.set('likes', videoData.likes)
// videoInstance.set('dislikes', videoData.dislikes)
await videoInstance.save(sequelizeOptions)
@ -101,36 +99,3 @@ async function updateRemoteVideo (account: AccountModel, videoAttributesToUpdate
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 { logger } from '../../../helpers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
import { processAcceptActivity } from './process-accept'
import { processAddActivity } from './process-add'
import { processAnnounceActivity } from './process-announce'
import { processCreateActivity } from './process-create'
import { processDeleteActivity } from './process-delete'
@ -11,9 +10,8 @@ import { processLikeActivity } from './process-like'
import { processUndoActivity } from './process-undo'
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,
Add: processAddActivity,
Update: processUpdateActivity,
Delete: processDeleteActivity,
Follow: processFollowActivity,
@ -23,11 +21,11 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
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) {
// When we fetch remote data, we don't have signature
if (signatureAccount && activity.actor !== signatureAccount.url) {
logger.warn('Signature mismatch between %s and %s.', activity.actor, signatureAccount.url)
if (signatureActor && activity.actor !== signatureActor.url) {
logger.warn('Signature mismatch between %s and %s.', activity.actor, signatureActor.url)
continue
}
@ -38,7 +36,7 @@ async function processActivities (activities: Activity[], signatureAccount?: Acc
}
try {
await activityProcessor(activity, inboxAccount)
await activityProcessor(activity, inboxActor)
} catch (err) {
logger.warn('Cannot process activity %s.', activity.type, err)
}

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import { Transaction } from 'sequelize'
import { ActivityAccept } from '../../../../shared/models/activitypub'
import { AccountModel } from '../../../models/account/account'
import { AccountFollowModel } from '../../../models/account/account-follow'
import { getAccountFollowAcceptActivityPubUrl } from '../url'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getActorFollowAcceptActivityPubUrl } from '../url'
import { unicastTo } from './misc'
async function sendAccept (accountFollow: AccountFollowModel, t: Transaction) {
const follower = accountFollow.AccountFollower
const me = accountFollow.AccountFollowing
async function sendAccept (actorFollow: ActorFollowModel, t: Transaction) {
const follower = actorFollow.ActorFollower
const me = actorFollow.ActorFollowing
const url = getAccountFollowAcceptActivityPubUrl(accountFollow)
const url = getActorFollowAcceptActivityPubUrl(actorFollow)
const data = acceptActivityData(url, me)
return unicastTo(data, me, follower.inboxUrl, t)
@ -23,12 +23,10 @@ export {
// ---------------------------------------------------------------------------
function acceptActivityData (url: string, byAccount: AccountModel) {
const activity: ActivityAccept = {
function acceptActivityData (url: string, byActor: ActorModel): ActivityAccept {
return {
type: 'Accept',
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 { ActivityAdd } from '../../../../shared/index'
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 { VideoChannelModel } from '../../../models/video/video-channel'
import { getAnnounceActivityPubUrl } from '../url'
import {
broadcastToFollowers,
getAccountsInvolvedInVideo,
getAccountsInvolvedInVideoChannel,
getActorsInvolvedInVideo,
getAudience,
getObjectFollowersAudience,
getOriginVideoAudience,
getOriginVideoChannelAudience,
unicastTo
} from './misc'
import { addActivityData } from './send-add'
import { createActivityData } from './send-create'
async function buildVideoAnnounceToFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(video.url, byAccount)
async function buildVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(video.url, byActor)
const videoObject = video.toActivityPubObject()
const videoChannel = video.VideoChannel
const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject(), t)
const announcedAudience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
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)
return announceActivityData(url, byAccount, announcedActivity, t, audience)
return announceActivityData(url, byActor, announcedActivity, t, audience)
}
async function sendVideoAnnounceToFollowers (byAccount: AccountModel, video: VideoModel, t: Transaction) {
const data = await buildVideoAnnounceToFollowers(byAccount, video, t)
async function sendVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) {
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) {
const url = getAnnounceActivityPubUrl(video.url, byAccount)
async function sendVideoAnnounceToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
const url = getAnnounceActivityPubUrl(video.url, byActor)
const videoChannel = video.VideoChannel
const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject(), t)
const videoObject = video.toActivityPubObject()
const announcedActivity = await createActivityData(url, video.VideoChannel.Account.Actor, videoObject, t)
const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
const data = await createActivityData(url, byAccount, announcedActivity, t, audience)
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
const data = await createActivityData(url, byActor, announcedActivity, t, audience)
return unicastTo(data, byAccount, videoChannel.Account.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)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl, t)
}
async function announceActivityData (
url: string,
byAccount: AccountModel,
object: ActivityCreate | ActivityAdd,
byActor: ActorModel,
object: ActivityCreate,
t: Transaction,
audience?: ActivityAudience
): Promise<ActivityAnnounce> {
if (!audience) {
audience = await getAudience(byAccount, t)
audience = await getAudience(byActor, t)
}
return {
@ -90,7 +61,7 @@ async function announceActivityData (
to: audience.to,
cc: audience.cc,
id: url,
actor: byAccount.url,
actor: byActor.url,
object
}
}
@ -99,10 +70,7 @@ async function announceActivityData (
export {
sendVideoAnnounceToFollowers,
sendVideoChannelAnnounceToFollowers,
sendVideoAnnounceToOrigin,
sendVideoChannelAnnounceToOrigin,
announceActivityData,
buildVideoAnnounceToFollowers,
buildVideoChannelAnnounceToFollowers
buildVideoAnnounceToFollowers
}

View File

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

View File

@ -1,54 +1,40 @@
import { Transaction } from 'sequelize'
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 { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoChannelShareModel } from '../../../models/video/video-channel-share'
import { VideoShareModel } from '../../../models/video/video-share'
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) {
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)
accountsInvolved.push(byAccount)
const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
actorsInvolved.push(byActor)
return broadcastToFollowers(data, byAccount, accountsInvolved, t)
return broadcastToFollowers(data, byActor, actorsInvolved, t)
}
async function sendDeleteAccount (account: AccountModel, t: Transaction) {
const data = deleteActivityData(account.url, account)
async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
const data = deleteActivityData(byActor.url, byActor)
return broadcastToFollowers(data, account, [ account ], t)
return broadcastToFollowers(data, byActor, [ byActor ], t)
}
// ---------------------------------------------------------------------------
export {
sendDeleteVideoChannel,
sendDeleteVideo,
sendDeleteAccount
sendDeleteActor
}
// ---------------------------------------------------------------------------
function deleteActivityData (url: string, byAccount: AccountModel): ActivityDelete {
function deleteActivityData (url: string, byActor: ActorModel): ActivityDelete {
return {
type: 'Delete',
id: url,
actor: byAccount.url
actor: byActor.url
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +1,20 @@
import { Transaction } from 'sequelize'
import { getServerAccount } from '../../helpers'
import { getServerActor } from '../../helpers'
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 { sendVideoAnnounceToFollowers, sendVideoChannelAnnounceToFollowers } 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)
}
import { sendVideoAnnounceToFollowers } from './send'
async function shareVideoByServer (video: VideoModel, t: Transaction) {
const serverAccount = await getServerAccount()
const serverActor = await getServerActor()
await VideoShareModel.create({
accountId: serverAccount.id,
actorId: serverActor.id,
videoId: video.id
}, { transaction: t })
return sendVideoAnnounceToFollowers(serverAccount, video, t)
return sendVideoAnnounceToFollowers(serverActor, video, t)
}
export {
shareVideoChannelByServer,
shareVideoByServer
}

View File

@ -1,16 +1,19 @@
import { CONFIG } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { AccountFollowModel } from '../../models/account/account-follow'
import { ActorModel } from '../../models/activitypub/actor'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { VideoModel } from '../../models/video/video'
import { VideoAbuseModel } from '../../models/video/video-abuse'
import { VideoChannelModel } from '../../models/video/video-channel'
function getVideoActivityPubUrl (video: VideoModel) {
return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
}
function getVideoChannelActivityPubUrl (videoChannel: VideoChannelModel) {
return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid
function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID
}
function getApplicationActivityPubUrl () {
return CONFIG.WEBSERVER.URL + '/application/peertube'
}
function getAccountActivityPubUrl (accountName: string) {
@ -21,34 +24,34 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
}
function getVideoViewActivityPubUrl (byAccount: AccountModel, video: VideoModel) {
return video.url + '/views/' + byAccount.uuid + '/' + new Date().toISOString()
function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString()
}
function getVideoLikeActivityPubUrl (byAccount: AccountModel, video: VideoModel) {
return byAccount.url + '/likes/' + video.id
function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return byActor.url + '/likes/' + video.id
}
function getVideoDislikeActivityPubUrl (byAccount: AccountModel, video: VideoModel) {
return byAccount.url + '/dislikes/' + video.id
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return byActor.url + '/dislikes/' + video.id
}
function getAccountFollowActivityPubUrl (accountFollow: AccountFollowModel) {
const me = accountFollow.AccountFollower
const following = accountFollow.AccountFollowing
function getActorFollowActivityPubUrl (actorFollow: ActorFollowModel) {
const me = actorFollow.ActorFollower
const following = actorFollow.ActorFollowing
return me.url + '/follows/' + following.id
}
function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowModel) {
const follower = accountFollow.AccountFollower
const me = accountFollow.AccountFollowing
function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
const follower = actorFollow.ActorFollower
const me = actorFollow.ActorFollowing
return follower.url + '/accepts/follows/' + me.id
}
function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountModel) {
return originalUrl + '/announces/' + byAccount.id
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) {
return originalUrl + '/announces/' + byActor.id
}
function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
@ -60,12 +63,13 @@ function getUndoActivityPubUrl (originalUrl: string) {
}
export {
getApplicationActivityPubUrl,
getVideoActivityPubUrl,
getVideoChannelActivityPubUrl,
getAccountActivityPubUrl,
getVideoAbuseActivityPubUrl,
getAccountFollowActivityPubUrl,
getAccountFollowAcceptActivityPubUrl,
getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl,
getAnnounceActivityPubUrl,
getUpdateActivityPubUrl,
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) {
// 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())
return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
@ -27,7 +27,7 @@ function fetchRemoteVideoPreview (video: VideoModel) {
async function fetchRemoteVideoDescription (video: VideoModel) {
// FIXME: use url
const host = video.VideoChannel.Account.Server.host
const host = video.VideoChannel.Account.Actor.Server.host
const path = video.getDescriptionPath()
const options = {
uri: REMOTE_SCHEME.HTTP + '://' + host + path,
@ -50,43 +50,47 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
}
async function sendVideoRateChangeToFollowers (
account: AccountModel,
account: AccountModel,
video: VideoModel,
likes: number,
dislikes: number,
t: Transaction
) {
const actor = account.Actor
// Keep the order: first we undo and then we create
// Undo Like
if (likes < 0) await sendUndoLikeToVideoFollowers(account, video, t)
if (likes < 0) await sendUndoLikeToVideoFollowers(actor, video, t)
// Undo Dislike
if (dislikes < 0) await sendUndoDislikeToVideoFollowers(account, video, t)
if (dislikes < 0) await sendUndoDislikeToVideoFollowers(actor, video, t)
// Like
if (likes > 0) await sendLikeToVideoFollowers(account, video, t)
if (likes > 0) await sendLikeToVideoFollowers(actor, video, t)
// Dislike
if (dislikes > 0) await sendCreateDislikeToVideoFollowers(account, video, t)
if (dislikes > 0) await sendCreateDislikeToVideoFollowers(actor, video, t)
}
async function sendVideoRateChangeToOrigin (
account: AccountModel,
account: AccountModel,
video: VideoModel,
likes: number,
dislikes: number,
t: Transaction
) {
const actor = account.Actor
// Keep the order: first we undo and then we create
// Undo Like
if (likes < 0) await sendUndoLikeToOrigin(account, video, t)
if (likes < 0) await sendUndoLikeToOrigin(actor, video, t)
// Undo Dislike
if (dislikes < 0) await sendUndoDislikeToOrigin(account, video, t)
if (dislikes < 0) await sendUndoDislikeToOrigin(actor, video, t)
// Like
if (likes > 0) await sendLikeToOrigin(account, video, t)
if (likes > 0) await sendLikeToOrigin(actor, video, t)
// Dislike
if (dislikes > 0) await sendCreateDislikeToOrigin(account, video, t)
if (dislikes > 0) await sendCreateDislikeToOrigin(actor, video, t)
}
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 { buildSignedActivity, logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
import { JobHandler, JobScheduler } from '../job-scheduler'
import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler'
@ -10,7 +10,7 @@ import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handl
type ActivityPubHttpPayload = {
uris: string[]
signatureAccountId?: number
signatureActorId?: number
body?: any
attemptNumber?: number
}
@ -44,10 +44,10 @@ function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, ur
async function computeBody (payload: ActivityPubHttpPayload) {
let body = payload.body
if (payload.signatureAccountId) {
const accountSignature = await AccountModel.load(payload.signatureAccountId)
if (!accountSignature) throw new Error('Unknown signature account id.')
body = await buildSignedActivity(accountSignature, payload.body)
if (payload.signatureActorId) {
const actorSignature = await ActorModel.load(payload.signatureActorId)
if (!actorSignature) throw new Error('Unknown signature account id.')
body = await buildSignedActivity(actorSignature, payload.body)
}
return body

View File

@ -3,7 +3,7 @@ import { computeResolutionsToTranscode, logger } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video'
import { shareVideoByServer } from '../../activitypub'
import { sendAddVideo } from '../../activitypub/send'
import { sendCreateVideo } from '../../activitypub/send'
import { JobScheduler } from '../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
// 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)
const originalFileHeight = await videoDatabase.getOriginalFileHeight()

View File

@ -1,10 +1,9 @@
import * as Sequelize from 'sequelize'
import { createPrivateAndPublicKeys, logger } from '../helpers'
import { CONFIG, sequelizeTypescript } from '../initializers'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../initializers'
import { AccountModel } from '../models/account/account'
import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor'
import { getAccountActivityPubUrl } from './activitypub'
import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
import { createVideoChannel } from './video-channel'
async function createUserAccountAndChannel (user: UserModel, validateUser = true) {
@ -26,31 +25,22 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true
return { account: accountCreated, videoChannel }
})
// Set account keys, this could be long so process after the account creation and do not block the client
const { publicKey, privateKey } = await createPrivateAndPublicKeys()
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))
account.Actor = await setAsyncActorKeys(account.Actor)
videoChannel.Actor = await setAsyncActorKeys(videoChannel.Actor)
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 actorInstance = new ActorModel({
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 actorInstance = buildActorInstance(type, url, name)
const actorInstanceCreated = await actorInstance.save({ transaction: t })
const accountInstance = new AccountModel({
@ -67,9 +57,18 @@ async function createLocalAccountWithoutKeys (name: string, userId: number, appl
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 {
createApplicationActor,
createUserAccountAndChannel,
createLocalAccountWithoutKeys
}

View File

@ -1,26 +1,33 @@
import * as Sequelize from 'sequelize'
import * as uuidv4 from 'uuid/v4'
import { VideoChannelCreate } from '../../shared/models'
import { AccountModel } from '../models/account/account'
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) {
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 = {
name: videoChannelInfo.name,
description: videoChannelInfo.description,
remote: false,
accountId: account.id
accountId: account.id,
actorId: actorInstanceCreated.id
}
const videoChannel = VideoChannelModel.build(videoChannelData)
videoChannel.set('url', getVideoChannelActivityPubUrl(videoChannel))
const options = { transaction: t }
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.Actor = actorInstanceCreated
// No need to seed this empty video channel to followers
return videoChannelCreated

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { body, param } from 'express-validator/check'
import { UserRight } from '../../../shared'
import { logger } from '../../helpers'
import { isAccountIdExist } from '../../helpers/custom-validators/accounts'
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import {
isVideoChannelDescriptionValid,
isVideoChannelExist,
@ -11,7 +11,6 @@ import {
} from '../../helpers/custom-validators/video-channels'
import { UserModel } from '../../models/account/user'
import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoChannelShareModel } from '../../models/video/video-channel-share'
import { areValidationErrors } from './utils'
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 {
@ -127,15 +104,13 @@ export {
videoChannelsAddValidator,
videoChannelsUpdateValidator,
videoChannelsRemoveValidator,
videoChannelsGetValidator,
videoChannelsShareValidator
videoChannelsGetValidator
}
// ---------------------------------------------------------------------------
function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) {
// Retrieve the user who did the request
if (videoChannel.isOwned() === false) {
if (videoChannel.Actor.isOwned() === false) {
res.status(403)
.json({ error: 'Cannot remove video channel of another server.' })
.end()

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { query } from 'express-validator/check'
import { logger } from '../../helpers'
import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { areValidationErrors } from './utils'
const webfingerValidator = [
@ -17,14 +17,14 @@ const webfingerValidator = [
const nameWithHost = req.query.resource.substr(5)
const [ name ] = nameWithHost.split('@')
const account = await AccountModel.loadLocalByName(name)
if (!account) {
const actor = await ActorModel.loadLocalByName(name)
if (!actor) {
return res.status(404)
.send({ error: 'Account not found' })
.send({ error: 'Actor not found' })
.end()
}
res.locals.account = account
res.locals.actor = actor
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,
Column,
CreatedAt,
DataType,
Default,
DefaultScope,
ForeignKey,
HasMany,
Is,
IsUUID,
Model,
Table,
UpdatedAt
} from 'sequelize-typescript'
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 { ApplicationModel } from '../application/application'
import { ServerModel } from '../server/server'
@ -24,31 +22,30 @@ import { throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel'
import { UserModel } from './user'
@Table({
tableName: 'account',
indexes: [
@DefaultScope({
include: [
{
fields: [ 'name' ]
},
{
fields: [ 'serverId' ]
},
{
fields: [ 'userId' ],
unique: true
},
{
fields: [ 'applicationId' ],
unique: true
},
{
fields: [ 'name', 'serverId', 'applicationId' ],
unique: true
model: () => ActorModel,
required: true,
include: [
{
model: () => ServerModel,
required: false
}
]
}
]
})
@Table({
tableName: 'account'
})
export class AccountModel extends Model<AccountModel> {
@AllowNull(false)
@Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
@Column
name: string
@CreatedAt
createdAt: Date
@ -89,7 +86,7 @@ export class AccountModel extends Model<AccountModel> {
},
onDelete: 'cascade'
})
Application: ApplicationModel
Account: ApplicationModel
@HasMany(() => VideoChannelModel, {
foreignKey: {
@ -103,32 +100,27 @@ export class AccountModel extends Model<AccountModel> {
@AfterDestroy
static sendDeleteIfOwned (instance: AccountModel) {
if (instance.isOwned()) {
return sendDeleteAccount(instance, undefined)
return sendDeleteActor(instance.Actor, undefined)
}
return undefined
}
static loadApplication () {
return AccountModel.findOne({
include: [
{
model: ApplicationModel,
required: true
}
]
})
}
static load (id: number) {
return AccountModel.findById(id)
}
static loadByUUID (uuid: string) {
const query = {
where: {
uuid
}
include: [
{
model: ActorModel,
required: true,
where: {
uuid
}
}
]
}
return AccountModel.findOne(query)
@ -156,25 +148,6 @@ export class AccountModel extends Model<AccountModel> {
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) {
const query = {
include: [
@ -192,29 +165,11 @@ export class AccountModel extends Model<AccountModel> {
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 () {
const actor = this.Actor.toFormattedJSON()
const account = {
id: this.id,
name: this.name,
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
@ -223,7 +178,7 @@ export class AccountModel extends Model<AccountModel> {
}
toActivityPubObject () {
return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account')
return this.Actor.toActivityPubObject(this.name, 'Account')
}
isOwned () {

View File

@ -1,26 +1,13 @@
import * as Sequelize from 'sequelize'
import {
AllowNull,
BeforeCreate,
BeforeUpdate,
Column, CreatedAt,
DataType,
Default, DefaultScope,
HasMany,
HasOne,
Is,
IsEmail,
Model, Scopes,
Table, UpdatedAt
AllowNull, BeforeCreate, BeforeUpdate, Column, CreatedAt, DataType, Default, DefaultScope, HasMany, HasOne, Is, IsEmail, Model,
Scopes, Table, UpdatedAt
} from 'sequelize-typescript'
import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
import { comparePassword, cryptPassword } from '../../helpers'
import {
comparePassword,
cryptPassword
} from '../../helpers'
import {
isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
isUserVideoQuotaValid, isUserAutoPlayVideoValid
isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
isUserVideoQuotaValid
} from '../../helpers/custom-validators/users'
import { OAuthTokenModel } from '../oauth/oauth-token'
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 * as Sequelize from 'sequelize'
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
} from 'sequelize-typescript'
import { ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { activityPubContextify } from '../../helpers'
import {
isActivityPubUrlValid,
isActorFollowersCountValid,
isActorFollowingCountValid, isActorPreferredUsernameValid,
isActorFollowingCountValid,
isActorNameValid,
isActorPrivateKeyValid,
isActorPublicKeyValid
} from '../../helpers/custom-validators/activitypub'
import { isUserUsernameValid } from '../../helpers/custom-validators/users'
import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AccountFollowModel } from '../account/account-follow'
import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AccountModel } from '../account/account'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server'
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({
tableName: 'actor'
tableName: 'actor',
indexes: [
{
fields: [ 'name', 'serverId' ],
unique: true
}
]
})
export class ActorModel extends Model<ActorModel> {
@AllowNull(false)
@Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
type: ActivityPubActorType
@AllowNull(false)
@Default(DataType.UUIDV4)
@IsUUID(4)
@ -32,7 +77,7 @@ export class ActorModel extends Model<ActorModel> {
uuid: string
@AllowNull(false)
@Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name'))
@Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name'))
@Column
name: string
@ -104,24 +149,24 @@ export class ActorModel extends Model<ActorModel> {
})
Avatar: AvatarModel
@HasMany(() => AccountFollowModel, {
@HasMany(() => ActorFollowModel, {
foreignKey: {
name: 'accountId',
name: 'actorId',
allowNull: false
},
onDelete: 'cascade'
})
AccountFollowing: AccountFollowModel[]
AccountFollowing: ActorFollowModel[]
@HasMany(() => AccountFollowModel, {
@HasMany(() => ActorFollowModel, {
foreignKey: {
name: 'targetAccountId',
name: 'targetActorId',
allowNull: false
},
as: 'followers',
onDelete: 'cascade'
})
AccountFollowers: AccountFollowModel[]
AccountFollowers: ActorFollowModel[]
@ForeignKey(() => ServerModel)
@Column
@ -135,6 +180,36 @@ export class ActorModel extends Model<ActorModel> {
})
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) {
const query = {
where: {
@ -145,7 +220,48 @@ export class ActorModel extends Model<ActorModel> {
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 () {
@ -167,6 +283,7 @@ export class ActorModel extends Model<ActorModel> {
return {
id: this.id,
uuid: this.uuid,
host,
score,
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
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
activityPubType = 'Group'
activityPubType = 'Group' as 'Group'
}
const json = {
type,
type: activityPubType,
id: this.url,
following: this.getFollowingUrl(),
followers: this.getFollowersUrl(),
inbox: this.inboxUrl,
outbox: this.outboxUrl,
preferredUsername: name,
preferredUsername,
url: this.url,
name,
name: this.name,
endpoints: {
sharedInbox: this.sharedInboxUrl
},
uuid,
uuid: this.uuid,
publicKey: {
id: this.getPublicKeyUrl(),
owner: this.url,
@ -212,11 +331,11 @@ export class ActorModel extends Model<ActorModel> {
attributes: [ 'sharedInboxUrl' ],
include: [
{
model: AccountFollowModel,
model: ActorFollowModel,
required: true,
as: 'followers',
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({
tableName: 'application'
})
@ -11,7 +20,19 @@ export class ApplicationModel extends Model<ApplicationModel> {
@Column
migrationVersion: number
@HasOne(() => AccountModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
Account: AccountModel
static countTotal () {
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 { CONFIG } from '../../initializers'
import { AccountModel } from '../account/account'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
@ -63,13 +62,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
include: [
{
model: AccountModel,
required: true,
include: [
{
model: ServerModel,
required: false
}
]
required: true
},
{
model: VideoModel,
@ -87,8 +80,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
toFormattedJSON () {
let reporterServerHost
if (this.Account.Server) {
reporterServerHost = this.Account.Server.host
if (this.Account.Actor.Server) {
reporterServerHost = this.Account.Actor.Server.host
} else {
// It means it's our video
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 {
AfterDestroy,
AllowNull,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
DefaultScope,
ForeignKey,
HasMany,
Is,
IsUUID,
Model,
Scopes,
Table,
UpdatedAt
} 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 { sendDeleteVideoChannel } from '../../lib/activitypub/send'
import { sendDeleteActor } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account'
import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
import { VideoChannelShareModel } from './video-channel-share'
enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACTOR = 'WITH_ACTOR',
WITH_VIDEOS = 'WITH_VIDEOS'
}
@DefaultScope({
include: [
{
model: () => ActorModel,
required: true
}
]
})
@Scopes({
[ScopeNames.WITH_ACCOUNT]: {
include: [
{
model: () => AccountModel,
include: [ { model: () => ServerModel, required: false } ]
required: true,
include: [
{
model: () => ActorModel,
required: true
}
]
}
]
},
@ -44,6 +54,11 @@ enum ScopeNames {
include: [
() => VideoModel
]
},
[ScopeNames.WITH_ACTOR]: {
include: [
() => ActorModel
]
}
})
@Table({
@ -56,12 +71,6 @@ enum ScopeNames {
})
export class VideoChannelModel extends Model<VideoChannelModel> {
@AllowNull(false)
@Default(DataType.UUIDV4)
@IsUUID(4)
@Column(DataType.UUID)
uuid: string
@AllowNull(false)
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
@Column
@ -72,10 +81,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
@Column
description: string
@AllowNull(false)
@Column
remote: boolean
@CreatedAt
createdAt: Date
@ -115,19 +120,10 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
})
Videos: VideoModel[]
@HasMany(() => VideoChannelShareModel, {
foreignKey: {
name: 'channelId',
allowNull: false
},
onDelete: 'CASCADE'
})
VideoChannelShares: VideoChannelShareModel[]
@AfterDestroy
static sendDeleteIfOwned (instance: VideoChannelModel) {
if (instance.isOwned()) {
return sendDeleteVideoChannel(instance, undefined)
if (instance.Actor.isOwned()) {
return sendDeleteActor(instance.Actor, undefined)
}
return undefined
@ -150,7 +146,9 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
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 }) => {
return { total: count, data: rows }
})
@ -165,51 +163,18 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
where: {
id: accountId
},
required: true,
include: [ { model: ServerModel, required: false } ]
required: true
}
]
}
return VideoChannelModel.findAndCountAll(query)
return VideoChannelModel
.findAndCountAll(query)
.then(({ rows, count }) => {
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) {
const options = {
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) {
return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findById(id)
return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
.findById(id)
}
static loadByUUIDAndPopulateAccount (uuid: string) {
const options = {
where: {
uuid
}
include: [
{
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) {
@ -242,39 +219,36 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
]
}
return VideoChannelModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]).findById(id, options)
}
isOwned () {
return this.remote === false
return VideoChannelModel
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
.findById(id, options)
}
toFormattedJSON () {
const json = {
const actor = this.Actor.toFormattedJSON()
const account = {
id: this.id,
uuid: this.uuid,
name: this.name,
description: this.description,
isLocal: this.isOwned(),
isLocal: this.Actor.isOwned(),
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
if (this.Account !== undefined) {
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
return Object.assign(actor, account)
}
toActivityPubObject () {
return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel')
toActivityPubObject (): ActivityPubActor {
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 { 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'
enum ScopeNames {
FULL = 'FULL',
WITH_ACCOUNT = 'WITH_ACCOUNT'
WITH_ACTOR = 'WITH_ACTOR'
}
@Scopes({
[ScopeNames.FULL]: {
include: [
{
model: () => AccountModel,
model: () => ActorModel,
required: true
},
{
@ -21,10 +21,10 @@ enum ScopeNames {
}
]
},
[ScopeNames.WITH_ACCOUNT]: {
[ScopeNames.WITH_ACTOR]: {
include: [
{
model: () => AccountModel,
model: () => ActorModel,
required: true
}
]
@ -34,7 +34,7 @@ enum ScopeNames {
tableName: 'videoShare',
indexes: [
{
fields: [ 'accountId' ]
fields: [ 'actorId' ]
},
{
fields: [ 'videoId' ]
@ -48,17 +48,17 @@ export class VideoShareModel extends Model<VideoShareModel> {
@UpdatedAt
updatedAt: Date
@ForeignKey(() => AccountModel)
@ForeignKey(() => ActorModel)
@Column
accountId: number
actorId: number
@BelongsTo(() => AccountModel, {
@BelongsTo(() => ActorModel, {
foreignKey: {
allowNull: false
},
onDelete: 'cascade'
})
Account: AccountModel
Actor: ActorModel
@ForeignKey(() => VideoModel)
@Column
@ -72,24 +72,24 @@ export class VideoShareModel extends Model<VideoShareModel> {
})
Video: VideoModel
static load (accountId: number, videoId: number, t: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACCOUNT).findOne({
static load (actorId: number, videoId: number, t: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: {
accountId,
actorId,
videoId
},
transaction: t
})
}
static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) {
static loadActorsByShare (videoId: number, t: Sequelize.Transaction) {
const query = {
where: {
videoId
},
include: [
{
model: AccountModel,
model: ActorModel,
required: true
}
],
@ -97,6 +97,6 @@ export class VideoShareModel extends Model<VideoShareModel> {
}
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
} from '../../initializers'
import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
import { sendDeleteVideo } from '../../lib/index'
import { sendDeleteVideo } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate'
import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils'
import { TagModel } from './tag'
@ -79,8 +80,7 @@ import { VideoShareModel } from './video-share'
import { VideoTagModel } from './video-tag'
enum ScopeNames {
NOT_IN_BLACKLIST = 'NOT_IN_BLACKLIST',
PUBLIC = 'PUBLIC',
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES',
@ -89,17 +89,13 @@ enum ScopeNames {
}
@Scopes({
[ScopeNames.NOT_IN_BLACKLIST]: {
[ScopeNames.AVAILABLE_FOR_LIST]: {
where: {
id: {
[Sequelize.Op.notIn]: Sequelize.literal(
'(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
)
}
}
},
[ScopeNames.PUBLIC]: {
where: {
},
privacy: VideoPrivacy.PUBLIC
}
},
@ -114,8 +110,14 @@ enum ScopeNames {
required: true,
include: [
{
model: () => ServerModel,
required: false
model: () => ActorModel,
required: true,
include: [
{
model: () => ServerModel,
required: false
}
]
}
]
}
@ -138,7 +140,7 @@ enum ScopeNames {
include: [
{
model: () => VideoShareModel,
include: [ () => AccountModel ]
include: [ () => ActorModel ]
}
]
},
@ -271,7 +273,7 @@ export class VideoModel extends Model<VideoModel> {
@BelongsTo(() => VideoChannelModel, {
foreignKey: {
allowNull: false
allowNull: true
},
onDelete: 'cascade'
})
@ -351,14 +353,15 @@ export class VideoModel extends Model<VideoModel> {
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) {
const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
'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" ' +
'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
'WHERE "VideoShare"."accountId" = ' + accountId
'WHERE "VideoShare"."actorId" = ' + actorId
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,
@ -469,7 +477,7 @@ export class VideoModel extends Model<VideoModel> {
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)
.then(({ rows, count }) => {
return {
@ -541,7 +549,13 @@ export class VideoModel extends Model<VideoModel> {
const accountInclude: IIncludeOptions = {
model: AccountModel,
include: [ serverInclude ]
include: [
{
model: ActorModel,
required: true,
include: [ serverInclude ]
}
]
}
const videoChannelInclude: IIncludeOptions = {
@ -586,7 +600,7 @@ export class VideoModel extends Model<VideoModel> {
videoChannelInclude, tagInclude
]
return VideoModel.scope([ ScopeNames.NOT_IN_BLACKLIST, ScopeNames.PUBLIC ])
return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST ])
.findAndCountAll(query).then(({ rows, count }) => {
return {
data: rows,
@ -688,8 +702,8 @@ export class VideoModel extends Model<VideoModel> {
toFormattedJSON () {
let serverHost
if (this.VideoChannel.Account.Server) {
serverHost = this.VideoChannel.Account.Server.host
if (this.VideoChannel.Account.Actor.Server) {
serverHost = this.VideoChannel.Account.Actor.Server.host
} else {
// It means it's our video
serverHost = CONFIG.WEBSERVER.HOST
@ -805,9 +819,9 @@ export class VideoModel extends Model<VideoModel> {
for (const rate of this.AccountVideoRates) {
if (rate.type === 'like') {
likes.push(rate.Account.url)
likes.push(rate.Account.Actor.url)
} 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[] = []
for (const videoShare of this.VideoShares) {
const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account)
const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Actor)
shares.push(shareUrl)
}
@ -886,7 +900,13 @@ export class VideoModel extends Model<VideoModel> {
url,
likes: likesObject,
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
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
} else {
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host
baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
}
return { baseUrlHttp, baseUrlWs }

View File

@ -184,25 +184,9 @@ describe('Test server follows API validators', function () {
.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 () {
await request(server.url)
.delete(path + '/-1')
.delete(path + '/example.com')
.set('Authorization', 'Bearer ' + server.accessToken)
.set('Accept', 'application/json')
.expect(404)

View File

@ -23,7 +23,6 @@ const expect = chai.expect
describe('Test follows', function () {
let servers: ServerInfo[] = []
let server3Id: number
before(async function () {
this.timeout(20000)
@ -82,8 +81,6 @@ describe('Test follows', function () {
expect(server3Follow).to.not.be.undefined
expect(server2Follow.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 () {
@ -121,7 +118,7 @@ describe('Test follows', function () {
it('Should unfollow server 3 on server 1', async function () {
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)
})

View File

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

View File

@ -1,14 +1,14 @@
import { ActivityPubSignature } from './activitypub-signature'
import { VideoChannelObject, VideoTorrentObject } from './objects'
import { VideoTorrentObject } from './objects'
import { DislikeObject } from './objects/dislike-object'
import { VideoAbuseObject } from './objects/video-abuse-object'
import { ViewObject } from './objects/view-object'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
export type Activity = ActivityCreate | ActivityUpdate |
ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
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 {
to: string[]
@ -27,18 +27,12 @@ export interface BaseActivity {
export interface ActivityCreate extends BaseActivity {
type: 'Create'
object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject
}
export interface ActivityAdd extends BaseActivity {
type: 'Add'
target: string
object: VideoTorrentObject
object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject
}
export interface ActivityUpdate extends BaseActivity {
type: 'Update'
object: VideoTorrentObject | VideoChannelObject
object: VideoTorrentObject
}
export interface ActivityDelete extends BaseActivity {
@ -56,7 +50,7 @@ export interface ActivityAccept extends BaseActivity {
export interface ActivityAnnounce extends BaseActivity {
type: 'Announce'
object: ActivityCreate | ActivityAdd
object: ActivityCreate
}
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 {
'@context': any[]
type: 'Person' | 'Application' | 'Group'
type: ActivityPubActorType
id: string
following: string
followers: string
@ -12,6 +16,8 @@ export interface ActivityPubActor {
endpoints: {
sharedInbox: string
}
summary: string
attributedTo: ActivityPubAttributedTo[]
uuid: string
publicKey: {
@ -21,7 +27,6 @@ export interface ActivityPubActor {
}
// Not used
// summary: string
// icon: string[]
// liked: string
}

View File

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

View File

@ -1,6 +1,5 @@
export * from './common-objects'
export * from './video-abuse-object'
export * from './video-channel-object'
export * from './video-torrent-object'
export * from './view-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 {
ActivityIconObject,
ActivityIdentifierObject,
ActivityIdentifierObject, ActivityPubAttributedTo,
ActivityTagObject,
ActivityUrlObject
} from './common-objects'
@ -24,8 +24,8 @@ export interface VideoTorrentObject {
content: string
icon: ActivityIconObject
url: ActivityUrlObject[]
actor?: string
likes?: ActivityPubOrderedCollection<string>
dislikes?: ActivityPubOrderedCollection<string>
shares?: ActivityPubOrderedCollection<string>
attributedTo: ActivityPubAttributedTo[]
}

View File

@ -1,4 +1,4 @@
export * from './accounts'
export * from './actors'
export * from './activitypub'
export * from './users'
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 { 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 { VideoPrivacy } from './video-privacy.enum'