From fadf619ad61a016c1c7fc53de5a8f398a4f77519 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 14 Dec 2017 11:18:49 +0100 Subject: [PATCH] Save --- server/controllers/activitypub/outbox.ts | 4 +- server/controllers/api/server/follows.ts | 2 +- server/controllers/api/videos/channel.ts | 11 +- server/controllers/webfinger.ts | 4 +- .../custom-validators/activitypub/account.ts | 92 ------- .../custom-validators/activitypub/activity.ts | 2 +- .../custom-validators/activitypub/actor.ts | 91 +++++++ .../custom-validators/activitypub/index.ts | 2 +- .../custom-validators/activitypub/undo.ts | 2 +- server/initializers/constants.ts | 2 +- server/lib/user.ts | 24 +- server/models/account/account.ts | 234 +++-------------- server/models/activitypub/actor.ts | 245 ++++++++++++++++++ server/models/video/video-channel.ts | 60 ++--- .../models/activitypub/activitypub-actor.ts | 2 +- 15 files changed, 426 insertions(+), 351 deletions(-) delete mode 100644 server/helpers/custom-validators/activitypub/account.ts create mode 100644 server/helpers/custom-validators/activitypub/actor.ts create mode 100644 server/models/activitypub/actor.ts diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index dc6b72a6e..6ed8a3454 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -40,14 +40,14 @@ async function outboxController (req: express.Request, res: express.Response, ne // This is a shared video const videoChannel = video.VideoChannel if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { - const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.url, videoObject, undefined) + const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.Actor.url, videoObject, undefined) const url = getAnnounceActivityPubUrl(video.url, account) const announceActivity = await announceActivityData(url, account, addActivity, undefined) activities.push(announceActivity) } else { - const addActivity = await addActivityData(video.url, account, video, videoChannel.url, videoObject, undefined) + const addActivity = await addActivityData(video.url, account, video, videoChannel.Actor.url, videoObject, undefined) activities.push(addActivity) } diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 913998e3a..497edb8eb 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts @@ -157,7 +157,7 @@ async function removeFollow (req: express.Request, res: express.Response, next: // This could be long so don't wait this task const following = follow.AccountFollowing following.destroy() - .catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.url, err)) + .catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.Actor.url, err)) return res.status(204).end() } diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts index 683b0448d..315469115 100644 --- a/server/controllers/api/videos/channel.ts +++ b/server/controllers/api/videos/channel.ts @@ -92,16 +92,15 @@ async function addVideoChannelRetryWrapper (req: express.Request, res: express.R return res.type('json').status(204).end() } -async function addVideoChannel (req: express.Request, res: express.Response) { +function addVideoChannel (req: express.Request, res: express.Response) { const videoChannelInfo: VideoChannelCreate = req.body const account: AccountModel = res.locals.oauth.token.User.Account - let videoChannelCreated: VideoChannelModel - await sequelizeTypescript.transaction(async t => { - videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t) - }) + return sequelizeTypescript.transaction(async t => { + const videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t) - logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid) + logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid) + }) } async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts index bb2ea40fa..8829500bc 100644 --- a/server/controllers/webfinger.ts +++ b/server/controllers/webfinger.ts @@ -23,12 +23,12 @@ function webfingerController (req: express.Request, res: express.Response, next: const json = { subject: req.query.resource, - aliases: [ account.url ], + aliases: [ account.Actor.url ], links: [ { rel: 'self', type: 'application/activity+json', - href: account.url + href: account.Actor.url } ] } diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts deleted file mode 100644 index 10bf81e8a..000000000 --- a/server/helpers/custom-validators/activitypub/account.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as validator from 'validator' -import { CONSTRAINTS_FIELDS } from '../../../initializers' -import { isAccountNameValid } from '../accounts' -import { exists, isUUIDValid } from '../misc' -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' - -function isAccountEndpointsObjectValid (endpointObject: any) { - return isActivityPubUrlValid(endpointObject.sharedInbox) -} - -function isAccountPublicKeyObjectValid (publicKeyObject: any) { - return isActivityPubUrlValid(publicKeyObject.id) && - isActivityPubUrlValid(publicKeyObject.owner) && - isAccountPublicKeyValid(publicKeyObject.publicKeyPem) -} - -function isAccountTypeValid (type: string) { - return type === 'Person' || type === 'Application' -} - -function isAccountPublicKeyValid (publicKey: string) { - return exists(publicKey) && - typeof publicKey === 'string' && - publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && - publicKey.endsWith('-----END PUBLIC KEY-----') && - validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY) -} - -function isAccountPreferredUsernameValid (preferredUsername: string) { - return isAccountNameValid(preferredUsername) -} - -function isAccountPrivateKeyValid (privateKey: string) { - return exists(privateKey) && - typeof privateKey === 'string' && - privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && - privateKey.endsWith('-----END RSA PRIVATE KEY-----') && - validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY) -} - -function isRemoteAccountValid (remoteAccount: any) { - return isActivityPubUrlValid(remoteAccount.id) && - isUUIDValid(remoteAccount.uuid) && - isAccountTypeValid(remoteAccount.type) && - isActivityPubUrlValid(remoteAccount.following) && - isActivityPubUrlValid(remoteAccount.followers) && - isActivityPubUrlValid(remoteAccount.inbox) && - isActivityPubUrlValid(remoteAccount.outbox) && - isAccountPreferredUsernameValid(remoteAccount.preferredUsername) && - isActivityPubUrlValid(remoteAccount.url) && - isAccountPublicKeyObjectValid(remoteAccount.publicKey) && - isAccountEndpointsObjectValid(remoteAccount.endpoints) -} - -function isAccountFollowingCountValid (value: string) { - return exists(value) && validator.isInt('' + value, { min: 0 }) -} - -function isAccountFollowersCountValid (value: string) { - return exists(value) && validator.isInt('' + value, { min: 0 }) -} - -function isAccountDeleteActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Delete') -} - -function isAccountFollowActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Follow') && - isActivityPubUrlValid(activity.object) -} - -function isAccountAcceptActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Accept') -} - -// --------------------------------------------------------------------------- - -export { - isAccountEndpointsObjectValid, - isAccountPublicKeyObjectValid, - isAccountTypeValid, - isAccountPublicKeyValid, - isAccountPreferredUsernameValid, - isAccountPrivateKeyValid, - isRemoteAccountValid, - isAccountFollowingCountValid, - isAccountFollowersCountValid, - isAccountNameValid, - isAccountFollowActivityValid, - isAccountAcceptActivityValid, - isAccountDeleteActivityValid -} diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 043e3c55e..ae7732194 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -1,6 +1,6 @@ import * as validator from 'validator' import { Activity, ActivityType } from '../../../../shared/models/activitypub' -import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' +import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './actor' import { isAnnounceActivityValid } from './announce' import { isActivityPubUrlValid } from './misc' import { isDislikeActivityValid, isLikeActivityValid } from './rate' diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts new file mode 100644 index 000000000..28551c96c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/actor.ts @@ -0,0 +1,91 @@ +import * as validator from 'validator' +import { CONSTRAINTS_FIELDS } from '../../../initializers' +import { isAccountNameValid } from '../accounts' +import { exists, isUUIDValid } from '../misc' +import { isActivityPubUrlValid, isBaseActivityValid } from './misc' + +function isActorEndpointsObjectValid (endpointObject: any) { + return isActivityPubUrlValid(endpointObject.sharedInbox) +} + +function isActorPublicKeyObjectValid (publicKeyObject: any) { + return isActivityPubUrlValid(publicKeyObject.id) && + isActivityPubUrlValid(publicKeyObject.owner) && + isActorPublicKeyValid(publicKeyObject.publicKeyPem) +} + +function isActorTypeValid (type: string) { + return type === 'Person' || type === 'Application' || type === 'Group' +} + +function isActorPublicKeyValid (publicKey: string) { + return exists(publicKey) && + typeof publicKey === 'string' && + publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && + publicKey.endsWith('-----END PUBLIC KEY-----') && + validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY) +} + +function isActorPreferredUsernameValid (preferredUsername: string) { + return isAccountNameValid(preferredUsername) +} + +function isActorPrivateKeyValid (privateKey: string) { + return exists(privateKey) && + typeof privateKey === 'string' && + privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && + privateKey.endsWith('-----END RSA PRIVATE KEY-----') && + validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY) +} + +function isRemoteActorValid (remoteActor: any) { + return isActivityPubUrlValid(remoteActor.id) && + isUUIDValid(remoteActor.uuid) && + isActorTypeValid(remoteActor.type) && + isActivityPubUrlValid(remoteActor.following) && + isActivityPubUrlValid(remoteActor.followers) && + isActivityPubUrlValid(remoteActor.inbox) && + isActivityPubUrlValid(remoteActor.outbox) && + isActorPreferredUsernameValid(remoteActor.preferredUsername) && + isActivityPubUrlValid(remoteActor.url) && + isActorPublicKeyObjectValid(remoteActor.publicKey) && + isActorEndpointsObjectValid(remoteActor.endpoints) +} + +function isActorFollowingCountValid (value: string) { + return exists(value) && validator.isInt('' + value, { min: 0 }) +} + +function isActorFollowersCountValid (value: string) { + return exists(value) && validator.isInt('' + value, { min: 0 }) +} + +function isActorDeleteActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Delete') +} + +function isActorFollowActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Follow') && + isActivityPubUrlValid(activity.object) +} + +function isActorAcceptActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Accept') +} + +// --------------------------------------------------------------------------- + +export { + isActorEndpointsObjectValid, + isActorPublicKeyObjectValid, + isActorTypeValid, + isActorPublicKeyValid, + isActorPreferredUsernameValid, + isActorPrivateKeyValid, + isRemoteActorValid, + isActorFollowingCountValid, + isActorFollowersCountValid, + isActorFollowActivityValid, + isActorAcceptActivityValid, + isActorDeleteActivityValid +} diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts index f8dfae4ff..ba411f1c6 100644 --- a/server/helpers/custom-validators/activitypub/index.ts +++ b/server/helpers/custom-validators/activitypub/index.ts @@ -1,4 +1,4 @@ -export * from './account' +export * from './actor' export * from './activity' export * from './misc' export * from './signature' diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts index 58043f8a1..d07bbf6b7 100644 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ b/server/helpers/custom-validators/activitypub/undo.ts @@ -1,4 +1,4 @@ -import { isAccountFollowActivityValid } from './account' +import { isAccountFollowActivityValid } from './actor' import { isBaseActivityValid } from './misc' import { isDislikeActivityValid, isLikeActivityValid } from './rate' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index ff322730f..f209bef90 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -131,7 +131,7 @@ const CONSTRAINTS_FIELDS = { FILE_SIZE: { min: 10 }, URL: { min: 3, max: 2000 } // Length }, - ACCOUNTS: { + ACTOR: { PUBLIC_KEY: { min: 10, max: 5000 }, // Length PRIVATE_KEY: { min: 10, max: 5000 }, // Length URL: { min: 3, max: 2000 } // Length diff --git a/server/lib/user.ts b/server/lib/user.ts index c4722fae2..6aeb198b9 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -3,6 +3,7 @@ import { createPrivateAndPublicKeys, logger } from '../helpers' import { CONFIG, sequelizeTypescript } 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 { createVideoChannel } from './video-channel' @@ -27,9 +28,10 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true // Set account keys, this could be long so process after the account creation and do not block the client const { publicKey, privateKey } = await createPrivateAndPublicKeys() - account.set('publicKey', publicKey) - account.set('privateKey', privateKey) - account.save().catch(err => logger.error('Cannot set public/private keys of local account %d.', account.id, err)) + const actor = account.Actor + actor.set('publicKey', publicKey) + actor.set('privateKey', privateKey) + actor.save().catch(err => logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)) return { account, videoChannel } } @@ -37,8 +39,7 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) { const url = getAccountActivityPubUrl(name) - const accountInstance = new AccountModel({ - name, + const actorInstance = new ActorModel({ url, publicKey: null, privateKey: null, @@ -48,13 +49,22 @@ async function createLocalAccountWithoutKeys (name: string, userId: number, appl outboxUrl: url + '/outbox', sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', followersUrl: url + '/followers', - followingUrl: url + '/following', + followingUrl: url + '/following' + }) + const actorInstanceCreated = await actorInstance.save({ transaction: t }) + + const accountInstance = new AccountModel({ + name, userId, applicationId, + actorId: actorInstanceCreated.id, serverId: null // It is our server }) - return accountInstance.save({ transaction: t }) + const accountInstanceCreated = await accountInstance.save({ transaction: t }) + accountInstanceCreated.Actor = actorInstanceCreated + + return accountInstanceCreated } // --------------------------------------------------------------------------- diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d6758fa10..b26395fd4 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,4 +1,3 @@ -import { join } from 'path' import * as Sequelize from 'sequelize' import { AfterDestroy, @@ -16,24 +15,13 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { Avatar } from '../../../shared/models/avatars/avatar.model' -import { activityPubContextify } from '../../helpers' -import { - isAccountFollowersCountValid, - isAccountFollowingCountValid, - isAccountPrivateKeyValid, - isAccountPublicKeyValid, - isActivityPubUrlValid -} from '../../helpers/custom-validators/activitypub' import { isUserUsernameValid } from '../../helpers/custom-validators/users' -import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { sendDeleteAccount } from '../../lib/activitypub/send' +import { ActorModel } from '../activitypub/actor' import { ApplicationModel } from '../application/application' -import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' import { throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' -import { AccountFollowModel } from './account-follow' import { UserModel } from './user' @Table({ @@ -59,68 +47,7 @@ import { UserModel } from './user' } ] }) -export class AccountModel extends Model { - - @AllowNull(false) - @Default(DataType.UUIDV4) - @IsUUID(4) - @Column(DataType.UUID) - uuid: string - - @AllowNull(false) - @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name')) - @Column - name: string - - @AllowNull(false) - @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - url: string - - @AllowNull(true) - @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max)) - publicKey: string - - @AllowNull(true) - @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max)) - privateKey: string - - @AllowNull(false) - @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count')) - @Column - followersCount: number - - @AllowNull(false) - @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count')) - @Column - followingCount: number - - @AllowNull(false) - @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - inboxUrl: string - - @AllowNull(false) - @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - outboxUrl: string - - @AllowNull(false) - @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - sharedInboxUrl: string - - @AllowNull(false) - @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - followersUrl: string - - @AllowNull(false) - @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - followingUrl: string +export class AccountModel extends Model { @CreatedAt createdAt: Date @@ -128,29 +55,17 @@ export class AccountModel extends Model { @UpdatedAt updatedAt: Date - @ForeignKey(() => AvatarModel) - @Column - avatarId: number - - @BelongsTo(() => AvatarModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Avatar: AvatarModel - - @ForeignKey(() => ServerModel) + @ForeignKey(() => ActorModel) @Column - serverId: number + actorId: number - @BelongsTo(() => ServerModel, { + @BelongsTo(() => ActorModel, { foreignKey: { - allowNull: true + allowNull: false }, onDelete: 'cascade' }) - Server: ServerModel + Actor: ActorModel @ForeignKey(() => UserModel) @Column @@ -185,25 +100,6 @@ export class AccountModel extends Model { }) VideoChannels: VideoChannelModel[] - @HasMany(() => AccountFollowModel, { - foreignKey: { - name: 'accountId', - allowNull: false - }, - onDelete: 'cascade' - }) - AccountFollowing: AccountFollowModel[] - - @HasMany(() => AccountFollowModel, { - foreignKey: { - name: 'targetAccountId', - allowNull: false - }, - as: 'followers', - onDelete: 'cascade' - }) - AccountFollowers: AccountFollowModel[] - @AfterDestroy static sendDeleteIfOwned (instance: AccountModel) { if (instance.isOwned()) { @@ -281,9 +177,15 @@ export class AccountModel extends Model { static loadByUrl (url: string, transaction?: Sequelize.Transaction) { const query = { - where: { - url - }, + include: [ + { + model: ActorModel, + required: true, + where: { + url + } + } + ], transaction } @@ -292,11 +194,17 @@ export class AccountModel extends Model { static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { const query = { - where: { - followersUrl: { - [ Sequelize.Op.in ]: followersUrls + include: [ + { + model: ActorModel, + required: true, + where: { + followersUrl: { + [ Sequelize.Op.in ]: followersUrls + } + } } - }, + ], transaction } @@ -304,97 +212,21 @@ export class AccountModel extends Model { } toFormattedJSON () { - let host = CONFIG.WEBSERVER.HOST - let score: number - let avatar: Avatar = null - - if (this.Avatar) { - avatar = { - path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), - createdAt: this.Avatar.createdAt, - updatedAt: this.Avatar.updatedAt - } - } - - if (this.Server) { - host = this.Server.host - score = this.Server.score - } - - return { + const actor = this.Actor.toFormattedJSON() + const account = { id: this.id, - uuid: this.uuid, - host, - score, - name: this.name, - followingCount: this.followingCount, - followersCount: this.followersCount, createdAt: this.createdAt, - updatedAt: this.updatedAt, - avatar + updatedAt: this.updatedAt } + + return Object.assign(actor, account) } toActivityPubObject () { - const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' - - const json = { - type, - id: this.url, - following: this.getFollowingUrl(), - followers: this.getFollowersUrl(), - inbox: this.inboxUrl, - outbox: this.outboxUrl, - preferredUsername: this.name, - url: this.url, - name: this.name, - endpoints: { - sharedInbox: this.sharedInboxUrl - }, - uuid: this.uuid, - publicKey: { - id: this.getPublicKeyUrl(), - owner: this.url, - publicKeyPem: this.publicKey - } - } - - return activityPubContextify(json) + return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account') } isOwned () { - return this.serverId === null - } - - getFollowerSharedInboxUrls (t: Sequelize.Transaction) { - const query = { - attributes: [ 'sharedInboxUrl' ], - include: [ - { - model: AccountFollowModel, - required: true, - as: 'followers', - where: { - targetAccountId: this.id - } - } - ], - transaction: t - } - - return AccountModel.findAll(query) - .then(accounts => accounts.map(a => a.sharedInboxUrl)) - } - - getFollowingUrl () { - return this.url + '/following' - } - - getFollowersUrl () { - return this.url + '/followers' - } - - getPublicKeyUrl () { - return this.url + '#main-key' + return this.Actor.isOwned() } } diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts new file mode 100644 index 000000000..4cae6a6ec --- /dev/null +++ b/server/models/activitypub/actor.ts @@ -0,0 +1,245 @@ +import { join } from 'path' +import * as Sequelize from 'sequelize' +import { + AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table, + UpdatedAt +} from 'sequelize-typescript' +import { Avatar } from '../../../shared/models/avatars/avatar.model' +import { activityPubContextify } from '../../helpers' +import { + isActivityPubUrlValid, + isActorFollowersCountValid, + isActorFollowingCountValid, isActorPreferredUsernameValid, + 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 { AvatarModel } from '../avatar/avatar' +import { ServerModel } from '../server/server' +import { throwIfNotValid } from '../utils' + +@Table({ + tableName: 'actor' +}) +export class ActorModel extends Model { + + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + uuid: string + + @AllowNull(false) + @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name')) + @Column + name: string + + @AllowNull(false) + @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + url: string + + @AllowNull(true) + @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY.max)) + publicKey: string + + @AllowNull(true) + @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY.max)) + privateKey: string + + @AllowNull(false) + @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count')) + @Column + followersCount: number + + @AllowNull(false) + @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count')) + @Column + followingCount: number + + @AllowNull(false) + @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + inboxUrl: string + + @AllowNull(false) + @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + outboxUrl: string + + @AllowNull(false) + @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + sharedInboxUrl: string + + @AllowNull(false) + @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + followersUrl: string + + @AllowNull(false) + @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max)) + followingUrl: string + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => AvatarModel) + @Column + avatarId: number + + @BelongsTo(() => AvatarModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Avatar: AvatarModel + + @HasMany(() => AccountFollowModel, { + foreignKey: { + name: 'accountId', + allowNull: false + }, + onDelete: 'cascade' + }) + AccountFollowing: AccountFollowModel[] + + @HasMany(() => AccountFollowModel, { + foreignKey: { + name: 'targetAccountId', + allowNull: false + }, + as: 'followers', + onDelete: 'cascade' + }) + AccountFollowers: AccountFollowModel[] + + @ForeignKey(() => ServerModel) + @Column + serverId: number + + @BelongsTo(() => ServerModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Server: ServerModel + + static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { + const query = { + where: { + followersUrl: { + [ Sequelize.Op.in ]: followersUrls + } + }, + transaction + } + + return ActorModel.findAll(query) + } + + toFormattedJSON () { + let avatar: Avatar = null + if (this.Avatar) { + avatar = { + path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), + createdAt: this.Avatar.createdAt, + updatedAt: this.Avatar.updatedAt + } + } + + let host = CONFIG.WEBSERVER.HOST + let score: number + if (this.Server) { + host = this.Server.host + score = this.Server.score + } + + return { + id: this.id, + host, + score, + followingCount: this.followingCount, + followersCount: this.followersCount, + avatar + } + } + + toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') { + let activityPubType + if (type === 'Account') { + activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' + } else { // VideoChannel + activityPubType = 'Group' + } + + const json = { + type, + id: this.url, + following: this.getFollowingUrl(), + followers: this.getFollowersUrl(), + inbox: this.inboxUrl, + outbox: this.outboxUrl, + preferredUsername: name, + url: this.url, + name, + endpoints: { + sharedInbox: this.sharedInboxUrl + }, + uuid, + publicKey: { + id: this.getPublicKeyUrl(), + owner: this.url, + publicKeyPem: this.publicKey + } + } + + return activityPubContextify(json) + } + + getFollowerSharedInboxUrls (t: Sequelize.Transaction) { + const query = { + attributes: [ 'sharedInboxUrl' ], + include: [ + { + model: AccountFollowModel, + required: true, + as: 'followers', + where: { + targetAccountId: this.id + } + } + ], + transaction: t + } + + return ActorModel.findAll(query) + .then(accounts => accounts.map(a => a.sharedInboxUrl)) + } + + getFollowingUrl () { + return this.url + '/following' + } + + getFollowersUrl () { + return this.url + '/followers' + } + + getPublicKeyUrl () { + return this.url + '#main-key' + } + + isOwned () { + return this.serverId === null + } +} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 068c8029d..fe44d3d53 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -11,18 +11,16 @@ import { HasMany, Is, IsUUID, - Model, Scopes, + Model, + Scopes, Table, UpdatedAt } from 'sequelize-typescript' import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' -import { activityPubCollection } from '../../helpers' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' -import { CONSTRAINTS_FIELDS } from '../../initializers' -import { getAnnounceActivityPubUrl } from '../../lib/activitypub' import { sendDeleteVideoChannel } 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' @@ -78,17 +76,24 @@ export class VideoChannelModel extends Model { @Column remote: boolean - @AllowNull(false) - @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max)) - url: string - @CreatedAt createdAt: Date @UpdatedAt updatedAt: Date + @ForeignKey(() => ActorModel) + @Column + actorId: number + + @BelongsTo(() => ActorModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + Actor: ActorModel + @ForeignKey(() => AccountModel) @Column accountId: number @@ -174,9 +179,15 @@ export class VideoChannelModel extends Model { static loadByUrl (url: string, t?: Sequelize.Transaction) { const query: IFindOptions = { - where: { - url - } + include: [ + { + model: ActorModel, + required: true, + where: { + url + } + } + ] } if (t !== undefined) query.transaction = t @@ -264,27 +275,6 @@ export class VideoChannelModel extends Model { } toActivityPubObject () { - let sharesObject - if (Array.isArray(this.VideoChannelShares)) { - const shares: string[] = [] - - for (const videoChannelShare of this.VideoChannelShares) { - const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) - shares.push(shareUrl) - } - - sharesObject = activityPubCollection(shares) - } - - return { - type: 'VideoChannel' as 'VideoChannel', - id: this.url, - uuid: this.uuid, - content: this.description, - name: this.name, - published: this.createdAt.toISOString(), - updated: this.updatedAt.toISOString(), - shares: sharesObject - } + return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel') } } diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index 77489135c..05b911d81 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts @@ -1,6 +1,6 @@ export interface ActivityPubActor { '@context': any[] - type: 'Person' | 'Application' + type: 'Person' | 'Application' | 'Group' id: string following: string followers: string -- 2.41.0