From 50d6de9c286abcb34ff4234d56d9cbb803db7665 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 14 Dec 2017 17:38:41 +0100 Subject: [PATCH] Begin moving video channel to actor --- .../followers-list.component.ts | 2 +- .../following-list.component.ts | 2 +- client/src/app/core/auth/auth.service.ts | 2 +- .../src/app/shared/account/account.model.ts | 2 +- .../app/shared/video/video-details.model.ts | 2 +- client/src/app/shared/video/video.model.ts | 2 +- scripts/update-host.ts | 8 +- server.ts | 3 +- server/controllers/activitypub/client.ts | 31 +-- server/controllers/activitypub/inbox.ts | 10 +- server/controllers/activitypub/outbox.ts | 20 +- server/controllers/api/server/follows.ts | 114 +++----- server/controllers/api/users.ts | 2 +- server/controllers/api/videos/abuse.ts | 20 +- server/controllers/api/videos/channel.ts | 28 +- server/controllers/api/videos/index.ts | 13 +- server/controllers/static.ts | 10 +- server/controllers/webfinger.ts | 8 +- server/helpers/activitypub.ts | 6 +- .../custom-validators/activitypub/activity.ts | 19 +- .../custom-validators/activitypub/actor.ts | 61 +++- .../custom-validators/activitypub/announce.ts | 6 +- .../custom-validators/activitypub/misc.ts | 20 +- .../custom-validators/activitypub/undo.ts | 4 +- .../activitypub/video-channels.ts | 6 - .../custom-validators/activitypub/videos.ts | 14 +- server/helpers/custom-validators/webfinger.ts | 8 +- server/helpers/peertube-crypto.ts | 18 +- server/helpers/utils.ts | 20 +- server/helpers/webfinger.ts | 33 ++- server/initializers/constants.ts | 14 +- server/initializers/database.ts | 8 +- server/initializers/installer.ts | 17 +- .../migrations/0100-activitypub.ts | 4 +- server/lib/activitypub/account.ts | 127 --------- server/lib/activitypub/actor.ts | 229 +++++++++++++++ server/lib/activitypub/fetch.ts | 6 +- server/lib/activitypub/index.ts | 3 +- server/lib/activitypub/process/index.ts | 1 - server/lib/activitypub/process/misc.ts | 52 +--- .../lib/activitypub/process/process-accept.ts | 18 +- server/lib/activitypub/process/process-add.ts | 137 --------- .../activitypub/process/process-announce.ts | 67 +---- .../lib/activitypub/process/process-create.ts | 187 +++++++++---- .../lib/activitypub/process/process-delete.ts | 72 +++-- .../lib/activitypub/process/process-follow.ts | 48 ++-- .../lib/activitypub/process/process-like.ts | 19 +- .../lib/activitypub/process/process-undo.ts | 43 +-- .../lib/activitypub/process/process-update.ts | 63 +---- server/lib/activitypub/process/process.ts | 14 +- server/lib/activitypub/send/index.ts | 1 - server/lib/activitypub/send/misc.ts | 80 +++--- server/lib/activitypub/send/send-accept.ts | 22 +- server/lib/activitypub/send/send-add.ts | 45 --- server/lib/activitypub/send/send-announce.ts | 84 ++---- server/lib/activitypub/send/send-create.ts | 107 +++---- server/lib/activitypub/send/send-delete.ts | 38 +-- server/lib/activitypub/send/send-follow.ts | 20 +- server/lib/activitypub/send/send-like.ts | 34 +-- server/lib/activitypub/send/send-undo.ts | 84 +++--- server/lib/activitypub/send/send-update.ts | 52 ++-- server/lib/activitypub/share.ts | 24 +- server/lib/activitypub/url.ts | 46 ++-- server/lib/activitypub/video-channels.ts | 59 ---- server/lib/activitypub/videos.ts | 28 +- server/lib/index.ts | 6 - .../activitypub-http-job-scheduler.ts | 12 +- .../video-file-optimizer-handler.ts | 5 +- server/lib/user.ts | 45 ++- server/lib/video-channel.ts | 19 +- server/middlewares/activitypub.ts | 28 +- server/middlewares/validators/follows.ts | 13 +- .../middlewares/validators/video-channels.ts | 31 +-- server/middlewares/validators/webfinger.ts | 10 +- server/models/account/account-follow.ts | 228 --------------- server/models/account/account.ts | 111 +++----- server/models/account/user.ts | 23 +- server/models/activitypub/actor-follow.ts | 260 ++++++++++++++++++ server/models/activitypub/actor.ts | 165 +++++++++-- server/models/application/application.ts | 23 +- server/models/video/video-abuse.ts | 13 +- server/models/video/video-channel-share.ts | 96 ------- server/models/video/video-channel.ts | 164 +++++------ server/models/video/video-share.ts | 32 +-- server/models/video/video.ts | 78 ++++-- server/tests/api/check-params/follows.ts | 18 +- server/tests/api/follows.ts | 5 +- server/tests/utils/follows.ts | 4 +- shared/models/activitypub/activity.ts | 18 +- .../models/activitypub/activitypub-actor.ts | 9 +- .../activitypub/objects/common-objects.ts | 5 + shared/models/activitypub/objects/index.ts | 1 - .../objects/video-channel-object.ts | 13 - .../objects/video-torrent-object.ts | 4 +- .../{accounts => actors}/account.model.ts | 0 .../{accounts => actors}/follow.model.ts | 0 shared/models/{accounts => actors}/index.ts | 0 shared/models/index.ts | 2 +- shared/models/users/user.model.ts | 2 +- shared/models/videos/video.model.ts | 2 +- 100 files changed, 1756 insertions(+), 2036 deletions(-) delete mode 100644 server/lib/activitypub/account.ts create mode 100644 server/lib/activitypub/actor.ts delete mode 100644 server/lib/activitypub/process/process-add.ts delete mode 100644 server/lib/activitypub/send/send-add.ts delete mode 100644 server/lib/activitypub/video-channels.ts delete mode 100644 server/lib/index.ts delete mode 100644 server/models/account/account-follow.ts create mode 100644 server/models/activitypub/actor-follow.ts delete mode 100644 server/models/video/video-channel-share.ts delete mode 100644 shared/models/activitypub/objects/video-channel-object.ts rename shared/models/{accounts => actors}/account.model.ts (100%) rename shared/models/{accounts => actors}/follow.model.ts (100%) rename shared/models/{accounts => actors}/index.ts (100%) diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index 8dc2d9317..649815709 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts @@ -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' diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index 411b8f640..d4f8d0309 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts @@ -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' diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 37264a8ad..c914848ae 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -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' diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts index 3a29ec979..bacaa208a 100644 --- a/client/src/app/shared/account/account.model.ts +++ b/client/src/app/shared/account/account.model.ts @@ -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' diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index d51bc01a7..8243b9f1c 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -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 { diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index c3759cb65..f159464c5 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -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 { diff --git a/scripts/update-host.ts b/scripts/update-host.ts index eccf203ea..4551a4702 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts @@ -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 diff --git a/server.ts b/server.ts index a89cdd69a..f64c4ac53 100644 --- a/server.ts +++ b/server.ts @@ -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 ----------- diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 72b216254..8c6294ff7 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -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) } diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index 88a0834f6..8332eabb1 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts @@ -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() } diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 6ed8a3454..01ba253c6 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -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() } diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 497edb8eb..e7d81f7c3 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts @@ -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[] = [] - 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 } -} diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 995542604..3106df9b9 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -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, diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index 08cc4d0b4..fecdaf5a3 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -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) } }) diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts index 315469115..cc00d9f8d 100644 --- a/server/controllers/api/videos/channel.ts +++ b/server/controllers/api/videos/channel.ts @@ -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) { diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 91ab8c66a..d6934748f 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -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) diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 33aed8927..ccae60517 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -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() diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts index 8829500bc..ed781c21b 100644 --- a/server/controllers/webfinger.ts +++ b/server/controllers/webfinger.ts @@ -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 } ] } diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 43907b596..5850fc19f 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -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 (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 + return signObject(byActor, activity) as Promise } // --------------------------------------------------------------------------- diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index ae7732194..c402800a4 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -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) { diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 28551c96c..bf42757c5 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts @@ -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 + + 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, 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 } diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts index 45f6b05a0..80511129c 100644 --- a/server/helpers/custom-validators/activitypub/announce.ts +++ b/server/helpers/custom-validators/activitypub/announce.ts @@ -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) ) } diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 65f5ca809..3ca4e4ff4 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts @@ -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 } diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts index d07bbf6b7..a2831b0bf 100644 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ b/server/helpers/custom-validators/activitypub/undo.ts @@ -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) ) diff --git a/server/helpers/custom-validators/activitypub/video-channels.ts b/server/helpers/custom-validators/activitypub/video-channels.ts index 9fd3bb149..eb45c6372 100644 --- a/server/helpers/custom-validators/activitypub/video-channels.ts +++ b/server/helpers/custom-validators/activitypub/video-channels.ts @@ -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 diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 2ed2988f5..b485e5fcf 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -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 diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts index 38f6b938d..c53db4027 100644 --- a/server/helpers/custom-validators/webfinger.ts +++ b/server/helpers/custom-validators/webfinger.ts @@ -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 } diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index c4c735cb8..a0c9112b9 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -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) diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index cb5e536b8..ef6a878cf 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -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, 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 } diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts index d98068cd7..76444fbe3 100644 --- a/server/helpers/webfinger.ts +++ b/server/helpers/webfinger.ts @@ -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,30 +11,33 @@ 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 - if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.') - - const selfLink = webfingerData.links.find(l => l.rel === 'self') - if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) { - 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 + const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host) + return getLinkOrThrow(webfingerData) } // --------------------------------------------------------------------------- export { - getAccountFromWebfinger + 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') + if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) { + throw new Error('Cannot find self link or href is not a valid URL.') + } + + return selfLink.href +} + function webfingerLookup (nameWithHost: string) { return new Promise((res, rej) => { webfinger.lookup(nameWithHost, (err, p) => { diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f209bef90..04b610b7a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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, diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 85d205cdc..0b3f695f7 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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, diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 5452743b6..ee3c9dfd9 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -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 { publicKey, privateKey } = await createPrivateAndPublicKeys() - accountCreated.set('publicKey', publicKey) - accountCreated.set('privateKey', privateKey) + const application = await ApplicationModel.create({ + migrationVersion: LAST_MIGRATION_VERSION + }) - return accountCreated.save() + return createApplicationActor(application.id) } diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts index fb42e1d57..d896b3205 100644 --- a/server/initializers/migrations/0100-activitypub.ts +++ b/server/initializers/migrations/0100-activitypub.ts @@ -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) diff --git a/server/lib/activitypub/account.ts b/server/lib/activitypub/account.ts deleted file mode 100644 index 45690b88d..000000000 --- a/server/lib/activitypub/account.ts +++ /dev/null @@ -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 | Promise { - 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 -} diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts new file mode 100644 index 000000000..c3de4bdce --- /dev/null +++ b/server/lib/activitypub/actor.ts @@ -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 | Promise { + 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 { + 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 }) +} diff --git a/server/lib/activitypub/fetch.ts b/server/lib/activitypub/fetch.ts index aa4dea8e0..4fc97cc38 100644 --- a/server/lib/activitypub/fetch.ts +++ b/server/lib/activitypub/fetch.ts @@ -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) diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts index fcea662a6..94ed1edaa 100644 --- a/server/lib/activitypub/index.ts +++ b/server/lib/activitypub/index.ts @@ -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' diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts index e25c261cc..db4980a72 100644 --- a/server/lib/activitypub/process/index.ts +++ b/server/lib/activitypub/process/index.ts @@ -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' diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts index a775c858a..a9c6f913c 100644 --- a/server/lib/activitypub/process/misc.ts +++ b/server/lib/activitypub/process/misc.ts @@ -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 } diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 5b321f771..b9d906ec9 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts @@ -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) } diff --git a/server/lib/activitypub/process/process-add.ts b/server/lib/activitypub/process/process-add.ts deleted file mode 100644 index 550593eab..000000000 --- a/server/lib/activitypub/process/process-add.ts +++ /dev/null @@ -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[] = 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[] = [] - - 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 -} diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index ff2c6d708..7dfee2f60 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts @@ -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) } diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index c1eb2a8ab..1ddd817db 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -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: [ 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[] = 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[] = [] + + 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: [ byAccount, activity ], + 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) { - 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) { +function processCreateVideoAbuse (actor: ActorModel, 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) { diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index 8f280d37f..65a4e5bcc 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts @@ -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.') - { - let videoObject = await VideoModel.loadByUrlAndPopulateAccount(activity.id) - if (videoObject !== undefined) { - return processDeleteVideo(account, videoObject) + 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 videoChannelObject = await VideoChannelModel.loadByUrl(activity.id) - if (videoChannelObject !== undefined) { - return processDeleteVideoChannel(account, videoChannelObject) + let videoObject = await VideoModel.loadByUrlAndPopulateAccount(activity.id) + if (videoObject !== undefined) { + 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,44 +63,40 @@ async function deleteRemoteVideo (account: AccountModel, videoToDelete: VideoMod logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) } -async function processDeleteVideoChannel (account: AccountModel, videoChannelToRemove: VideoChannelModel) { +async function processDeleteAccount (accountToRemove: AccountModel) { const options = { - arguments: [ account, videoChannelToRemove ], - errorMessage: 'Cannot remove the remote video channel with many retries.' + arguments: [ accountToRemove ], + errorMessage: 'Cannot remove the remote account with many retries.' } - await retryTransactionWrapper(deleteRemoteVideoChannel, options) + await retryTransactionWrapper(deleteRemoteAccount, options) } -async function deleteRemoteVideoChannel (account: AccountModel, videoChannelToRemove: VideoChannelModel) { - logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid) +async function deleteRemoteAccount (accountToRemove: AccountModel) { + logger.debug('Removing remote account "%s".', accountToRemove.Actor.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 }) + await accountToRemove.destroy({ transaction: t }) }) - logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid) + logger.info('Remote account with uuid %s removed.', accountToRemove.Actor.uuid) } -async function processDeleteAccount (accountToRemove: AccountModel) { +async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) { const options = { - arguments: [ accountToRemove ], - errorMessage: 'Cannot remove the remote account with many retries.' + arguments: [ videoChannelToRemove ], + errorMessage: 'Cannot remove the remote video channel with many retries.' } - await retryTransactionWrapper(deleteRemoteAccount, options) + await retryTransactionWrapper(deleteRemoteVideoChannel, options) } -async function deleteRemoteAccount (accountToRemove: AccountModel) { - logger.debug('Removing remote account "%s".', accountToRemove.uuid) +async function deleteRemoteVideoChannel (videoChannelToRemove: VideoChannelModel) { + logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.uuid) await sequelizeTypescript.transaction(async t => { - await accountToRemove.destroy({ transaction: t }) + await videoChannelToRemove.destroy({ transaction: t }) }) - logger.info('Remote account with uuid %s removed.', accountToRemove.uuid) + logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.Actor.uuid) } diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index ccaee43a6..ec7a331f3 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -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) } diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index a6e391f1e..a7fcec21c 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts @@ -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) } }) diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index efa63122b..4a0181137 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -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 }) diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 771021f0c..35912ee87 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts @@ -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) -} diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index bfbf8053c..dfb60c1bf 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts @@ -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 } = { +const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxActor?: ActorModel) => Promise } = { 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) } diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts index ee8f3ad7e..79ba6c7fe 100644 --- a/server/lib/activitypub/send/index.ts +++ b/server/lib/activitypub/send/index.ts @@ -1,5 +1,4 @@ export * from './send-accept' -export * from './send-add' export * from './send-announce' export * from './send-create' export * from './send-delete' diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index ffc221477..14101e630 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts @@ -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) - - return accountsToForwardView -} - -async function getAccountsInvolvedInVideoChannel (videoChannel: VideoChannelModel, t: Transaction) { - const accountsToForwardView = await VideoChannelShareModel.loadAccountsByShare(videoChannel.id, t) - accountsToForwardView.push(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 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 } diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts index f160af3c9..7579884a7 100644 --- a/server/lib/activitypub/send/send-accept.ts +++ b/server/lib/activitypub/send/send-accept.ts @@ -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 } diff --git a/server/lib/activitypub/send/send-add.ts b/server/lib/activitypub/send/send-add.ts deleted file mode 100644 index fd614db75..000000000 --- a/server/lib/activitypub/send/send-add.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index e685323e8..578fbc630 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts @@ -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 { 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 } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 9fbaa8196..d26c24838 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -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 { 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 } } diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index 0a45ea10f..4bc5db77e 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts @@ -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 } } diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts index 51735ddfd..eac60e94f 100644 --- a/server/lib/activitypub/send/send-follow.ts +++ b/server/lib/activitypub/send/send-follow.ts @@ -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 } } diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 1a35d0db0..7e0c73796 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -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 { 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 diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 699f920f0..92271b700 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -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 { 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 diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 9baf13a87..48bbbcac1 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -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 data = await updateActivityData(url, byActor, videoObject, t, audience) - 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) } // --------------------------------------------------------------------------- export { - sendUpdateVideoChannel, sendUpdateVideo } // --------------------------------------------------------------------------- -async function updateActivityData (url: string, byAccount: AccountModel, object: any, t: Transaction): Promise { - const { to, cc } = await getAudience(byAccount, t) +async function updateActivityData ( + url: string, + byActor: ActorModel, + object: any, + t: Transaction, + audience?: ActivityAudience +): Promise { + 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 } } diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 5bec61c05..f79c4e532 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts @@ -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 } diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 00b4e8852..bb2d4d11e 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -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, diff --git a/server/lib/activitypub/video-channels.ts b/server/lib/activitypub/video-channels.ts deleted file mode 100644 index c05a46f95..000000000 --- a/server/lib/activitypub/video-channels.ts +++ /dev/null @@ -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 -} diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 14c07fec0..fab43757a 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -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 { diff --git a/server/lib/index.ts b/server/lib/index.ts deleted file mode 100644 index d22ecb665..000000000 --- a/server/lib/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './activitypub' -export * from './cache' -export * from './jobs' -export * from './oauth-model' -export * from './user' -export * from './video-channel' diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts index 76da5b724..95a5d3ff2 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts @@ -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 diff --git a/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts index 1786ce971..7df048006 100644 --- a/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts +++ b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts @@ -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() diff --git a/server/lib/user.ts b/server/lib/user.ts index 6aeb198b9..ec1466c6f 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -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 } diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 97924aa9e..569b8f29d 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts @@ -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 diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index 489396447..37b7c42ec 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts @@ -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() diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 10482e5d0..2240d30db 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -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) diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index 068fd210f..cc7d54c06 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -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() diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index 7903c7400..2c8351799 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts @@ -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() } ] diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts deleted file mode 100644 index 975e7ee7d..000000000 --- a/server/models/account/account-follow.ts +++ /dev/null @@ -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 { - - @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[] = [] - - 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 - } - } -} diff --git a/server/models/account/account.ts b/server/models/account/account.ts index b26395fd4..1ee232537 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -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: [ - { - fields: [ 'name' ] - }, - { - fields: [ 'serverId' ] - }, - { - fields: [ 'userId' ], - unique: true - }, - { - fields: [ 'applicationId' ], - unique: true - }, +@DefaultScope({ + include: [ { - fields: [ 'name', 'serverId', 'applicationId' ], - unique: true + model: () => ActorModel, + required: true, + include: [ + { + model: () => ServerModel, + required: false + } + ] } ] }) +@Table({ + tableName: 'account' +}) export class AccountModel extends Model { + @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 { }, onDelete: 'cascade' }) - Application: ApplicationModel + Account: ApplicationModel @HasMany(() => VideoChannelModel, { foreignKey: { @@ -103,32 +100,27 @@ export class AccountModel extends Model { @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 { 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 { 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 { } toActivityPubObject () { - return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account') + return this.Actor.toActivityPubObject(this.name, 'Account') } isOwned () { diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 70ed61e07..1d5759ea3 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -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' diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts new file mode 100644 index 000000000..4cba05e95 --- /dev/null +++ b/server/models/activitypub/actor-follow.ts @@ -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 { + + @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[] = [] + + 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 + } + } +} diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 4cae6a6ec..ecaa43dcf 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -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 { + @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 { 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 { }) 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 { }) 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 { 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 { return { id: this.id, + uuid: this.uuid, host, score, followingCount: this.followingCount, @@ -175,28 +292,30 @@ export class ActorModel extends Model { } } - 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 { attributes: [ 'sharedInboxUrl' ], include: [ { - model: AccountFollowModel, + model: ActorFollowModel, required: true, as: 'followers', where: { - targetAccountId: this.id + targetActorId: this.id } } ], diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 9fc07e850..854a5fb36 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts @@ -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 { @Column migrationVersion: number + @HasOne(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Account: AccountModel + static countTotal () { return ApplicationModel.count() } + + static load () { + return ApplicationModel.findOne() + } } diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index d0ee969fb..182971c4e 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -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 { include: [ { model: AccountModel, - required: true, - include: [ - { - model: ServerModel, - required: false - } - ] + required: true }, { model: VideoModel, @@ -87,8 +80,8 @@ export class VideoAbuseModel extends Model { 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 diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts deleted file mode 100644 index f5b7a7cd5..000000000 --- a/server/models/video/video-channel-share.ts +++ /dev/null @@ -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 { - @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)) - } -} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index fe44d3d53..acc2486b3 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -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 { - @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 { @Column description: string - @AllowNull(false) - @Column - remote: boolean - @CreatedAt createdAt: Date @@ -115,19 +120,10 @@ export class VideoChannelModel extends Model { }) 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 { 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 { 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 = { - 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 = { - 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 { } } - 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 { ] } - 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 + } + ] + }) } } diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index e1733b3a7..c252fd646 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -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 { @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 { }) 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 { } return VideoShareModel.scope(ScopeNames.FULL).findAll(query) - .then(res => res.map(r => r.Account)) + .then(res => res.map(r => r.Actor)) } } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1f940a50d..97fdbc8ef 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 { @BelongsTo(() => VideoChannelModel, { foreignKey: { - allowNull: false + allowNull: true }, onDelete: 'cascade' }) @@ -351,14 +353,15 @@ export class VideoModel extends Model { 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 { } }, { - accountId + actorId } ] }, - include: [ AccountModel ] + include: [ + { + model: ActorModel, + required: true + } + ] }, { model: VideoChannelModel, @@ -469,7 +477,7 @@ export class VideoModel extends Model { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 } diff --git a/server/tests/api/check-params/follows.ts b/server/tests/api/check-params/follows.ts index 0af1562f5..cdd2783df 100644 --- a/server/tests/api/check-params/follows.ts +++ b/server/tests/api/check-params/follows.ts @@ -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) diff --git a/server/tests/api/follows.ts b/server/tests/api/follows.ts index dcb4c8bd9..10eb48969 100644 --- a/server/tests/api/follows.ts +++ b/server/tests/api/follows.ts @@ -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) }) diff --git a/server/tests/utils/follows.ts b/server/tests/utils/follows.ts index 033c6a719..a9f798bcb 100644 --- a/server/tests/utils/follows.ts +++ b/server/tests/utils/follows.ts @@ -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) diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 37f5400b9..1d248d3d7 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -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 { diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index 05b911d81..d9f80b94c 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts @@ -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 } diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 3eaab21b5..ea5a503ac 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts @@ -23,3 +23,8 @@ export interface ActivityUrlObject { width: number size?: number } + +export interface ActivityPubAttributedTo { + type: 'Group' | 'Person' + id: string +} diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index f1f761e44..3efd3ef13 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts @@ -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' diff --git a/shared/models/activitypub/objects/video-channel-object.ts b/shared/models/activitypub/objects/video-channel-object.ts deleted file mode 100644 index dcce8696b..000000000 --- a/shared/models/activitypub/objects/video-channel-object.ts +++ /dev/null @@ -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 -} diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts index a15ec7142..1405f7748 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-torrent-object.ts @@ -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 dislikes?: ActivityPubOrderedCollection shares?: ActivityPubOrderedCollection + attributedTo: ActivityPubAttributedTo[] } diff --git a/shared/models/accounts/account.model.ts b/shared/models/actors/account.model.ts similarity index 100% rename from shared/models/accounts/account.model.ts rename to shared/models/actors/account.model.ts diff --git a/shared/models/accounts/follow.model.ts b/shared/models/actors/follow.model.ts similarity index 100% rename from shared/models/accounts/follow.model.ts rename to shared/models/actors/follow.model.ts diff --git a/shared/models/accounts/index.ts b/shared/models/actors/index.ts similarity index 100% rename from shared/models/accounts/index.ts rename to shared/models/actors/index.ts diff --git a/shared/models/index.ts b/shared/models/index.ts index faf616bfc..a88c01608 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -1,4 +1,4 @@ -export * from './accounts' +export * from './actors' export * from './activitypub' export * from './users' export * from './videos' diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index f2b43d371..b5f459f31 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -1,4 +1,4 @@ -import { Account } from '../accounts' +import { Account } from '../actors' import { VideoChannel } from '../videos/video-channel.model' import { UserRole } from './user-role' diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 3a378419f..13b9c49b3 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -1,4 +1,4 @@ -import { Account } from '../accounts' +import { Account } from '../actors' import { VideoChannel } from './video-channel.model' import { VideoPrivacy } from './video-privacy.enum' -- 2.41.0