pull/161/head
Chocobozzz 2017-12-14 11:18:49 +01:00
parent 7efe153b0b
commit fadf619ad6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
15 changed files with 426 additions and 351 deletions

View File

@ -40,14 +40,14 @@ async function outboxController (req: express.Request, res: express.Response, ne
// This is a shared video // This is a shared video
const videoChannel = video.VideoChannel const videoChannel = video.VideoChannel
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.url, videoObject, undefined) const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.Actor.url, videoObject, undefined)
const url = getAnnounceActivityPubUrl(video.url, account) const url = getAnnounceActivityPubUrl(video.url, account)
const announceActivity = await announceActivityData(url, account, addActivity, undefined) const announceActivity = await announceActivityData(url, account, addActivity, undefined)
activities.push(announceActivity) activities.push(announceActivity)
} else { } 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) activities.push(addActivity)
} }

View File

@ -157,7 +157,7 @@ async function removeFollow (req: express.Request, res: express.Response, next:
// This could be long so don't wait this task // This could be long so don't wait this task
const following = follow.AccountFollowing const following = follow.AccountFollowing
following.destroy() 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() return res.status(204).end()
} }

View File

@ -92,16 +92,15 @@ async function addVideoChannelRetryWrapper (req: express.Request, res: express.R
return res.type('json').status(204).end() 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 videoChannelInfo: VideoChannelCreate = req.body
const account: AccountModel = res.locals.oauth.token.User.Account const account: AccountModel = res.locals.oauth.token.User.Account
let videoChannelCreated: VideoChannelModel
await sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
videoChannelCreated = await createVideoChannel(videoChannelInfo, account, 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) { async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -23,12 +23,12 @@ function webfingerController (req: express.Request, res: express.Response, next:
const json = { const json = {
subject: req.query.resource, subject: req.query.resource,
aliases: [ account.url ], aliases: [ account.Actor.url ],
links: [ links: [
{ {
rel: 'self', rel: 'self',
type: 'application/activity+json', type: 'application/activity+json',
href: account.url href: account.Actor.url
} }
] ]
} }

View File

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

View File

@ -1,6 +1,6 @@
import * as validator from 'validator' import * as validator from 'validator'
import { Activity, ActivityType } from '../../../../shared/models/activitypub' import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './actor'
import { isAnnounceActivityValid } from './announce' import { isAnnounceActivityValid } from './announce'
import { isActivityPubUrlValid } from './misc' import { isActivityPubUrlValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isDislikeActivityValid, isLikeActivityValid } from './rate'

View File

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

View File

@ -1,4 +1,4 @@
export * from './account' export * from './actor'
export * from './activity' export * from './activity'
export * from './misc' export * from './misc'
export * from './signature' export * from './signature'

View File

@ -1,4 +1,4 @@
import { isAccountFollowActivityValid } from './account' import { isAccountFollowActivityValid } from './actor'
import { isBaseActivityValid } from './misc' import { isBaseActivityValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isDislikeActivityValid, isLikeActivityValid } from './rate'

View File

@ -131,7 +131,7 @@ const CONSTRAINTS_FIELDS = {
FILE_SIZE: { min: 10 }, FILE_SIZE: { min: 10 },
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 } // Length
}, },
ACCOUNTS: { ACTOR: {
PUBLIC_KEY: { min: 10, max: 5000 }, // Length PUBLIC_KEY: { min: 10, max: 5000 }, // Length
PRIVATE_KEY: { min: 10, max: 5000 }, // Length PRIVATE_KEY: { min: 10, max: 5000 }, // Length
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 } // Length

View File

@ -3,6 +3,7 @@ import { createPrivateAndPublicKeys, logger } from '../helpers'
import { CONFIG, sequelizeTypescript } from '../initializers' import { CONFIG, sequelizeTypescript } from '../initializers'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor'
import { getAccountActivityPubUrl } from './activitypub' import { getAccountActivityPubUrl } from './activitypub'
import { createVideoChannel } from './video-channel' 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 // 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 { publicKey, privateKey } = await createPrivateAndPublicKeys()
account.set('publicKey', publicKey) const actor = account.Actor
account.set('privateKey', privateKey) actor.set('publicKey', publicKey)
account.save().catch(err => logger.error('Cannot set public/private keys of local account %d.', account.id, err)) actor.set('privateKey', privateKey)
actor.save().catch(err => logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err))
return { account, videoChannel } return { account, videoChannel }
} }
@ -37,8 +39,7 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true
async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) { async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) {
const url = getAccountActivityPubUrl(name) const url = getAccountActivityPubUrl(name)
const accountInstance = new AccountModel({ const actorInstance = new ActorModel({
name,
url, url,
publicKey: null, publicKey: null,
privateKey: null, privateKey: null,
@ -48,13 +49,22 @@ async function createLocalAccountWithoutKeys (name: string, userId: number, appl
outboxUrl: url + '/outbox', outboxUrl: url + '/outbox',
sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
followersUrl: url + '/followers', followersUrl: url + '/followers',
followingUrl: url + '/following', followingUrl: url + '/following'
})
const actorInstanceCreated = await actorInstance.save({ transaction: t })
const accountInstance = new AccountModel({
name,
userId, userId,
applicationId, applicationId,
actorId: actorInstanceCreated.id,
serverId: null // It is our server serverId: null // It is our server
}) })
return accountInstance.save({ transaction: t }) const accountInstanceCreated = await accountInstance.save({ transaction: t })
accountInstanceCreated.Actor = actorInstanceCreated
return accountInstanceCreated
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,4 +1,3 @@
import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { import {
AfterDestroy, AfterDestroy,
@ -16,24 +15,13 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } 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 { isUserUsernameValid } from '../../helpers/custom-validators/users'
import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { sendDeleteAccount } from '../../lib/activitypub/send' import { sendDeleteAccount } from '../../lib/activitypub/send'
import { ActorModel } from '../activitypub/actor'
import { ApplicationModel } from '../application/application' import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { throwIfNotValid } from '../utils' import { throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel' import { VideoChannelModel } from '../video/video-channel'
import { AccountFollowModel } from './account-follow'
import { UserModel } from './user' import { UserModel } from './user'
@Table({ @Table({
@ -59,68 +47,7 @@ import { UserModel } from './user'
} }
] ]
}) })
export class AccountModel extends Model<Account> { export class AccountModel extends Model<AccountModel> {
@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
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -128,29 +55,17 @@ export class AccountModel extends Model<Account> {
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
@ForeignKey(() => AvatarModel) @ForeignKey(() => ActorModel)
@Column @Column
avatarId: number actorId: number
@BelongsTo(() => AvatarModel, { @BelongsTo(() => ActorModel, {
foreignKey: { foreignKey: {
allowNull: true allowNull: false
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
Avatar: AvatarModel Actor: ActorModel
@ForeignKey(() => ServerModel)
@Column
serverId: number
@BelongsTo(() => ServerModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
Server: ServerModel
@ForeignKey(() => UserModel) @ForeignKey(() => UserModel)
@Column @Column
@ -185,25 +100,6 @@ export class AccountModel extends Model<Account> {
}) })
VideoChannels: VideoChannelModel[] 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 @AfterDestroy
static sendDeleteIfOwned (instance: AccountModel) { static sendDeleteIfOwned (instance: AccountModel) {
if (instance.isOwned()) { if (instance.isOwned()) {
@ -281,9 +177,15 @@ export class AccountModel extends Model<Account> {
static loadByUrl (url: string, transaction?: Sequelize.Transaction) { static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
const query = { const query = {
where: { include: [
url {
}, model: ActorModel,
required: true,
where: {
url
}
}
],
transaction transaction
} }
@ -292,11 +194,17 @@ export class AccountModel extends Model<Account> {
static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query = { const query = {
where: { include: [
followersUrl: { {
[ Sequelize.Op.in ]: followersUrls model: ActorModel,
required: true,
where: {
followersUrl: {
[ Sequelize.Op.in ]: followersUrls
}
}
} }
}, ],
transaction transaction
} }
@ -304,97 +212,21 @@ export class AccountModel extends Model<Account> {
} }
toFormattedJSON () { toFormattedJSON () {
let host = CONFIG.WEBSERVER.HOST const actor = this.Actor.toFormattedJSON()
let score: number const account = {
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 {
id: this.id, id: this.id,
uuid: this.uuid,
host,
score,
name: this.name,
followingCount: this.followingCount,
followersCount: this.followersCount,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt
avatar
} }
return Object.assign(actor, account)
} }
toActivityPubObject () { toActivityPubObject () {
const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account')
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)
} }
isOwned () { isOwned () {
return this.serverId === null return this.Actor.isOwned()
}
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'
} }
} }

View File

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

View File

@ -11,18 +11,16 @@ import {
HasMany, HasMany,
Is, Is,
IsUUID, IsUUID,
Model, Scopes, Model,
Scopes,
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' 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 { 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 { sendDeleteVideoChannel } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
@ -78,17 +76,24 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
@Column @Column
remote: boolean remote: boolean
@AllowNull(false)
@Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max))
url: string
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
@ForeignKey(() => ActorModel)
@Column
actorId: number
@BelongsTo(() => ActorModel, {
foreignKey: {
allowNull: false
},
onDelete: 'cascade'
})
Actor: ActorModel
@ForeignKey(() => AccountModel) @ForeignKey(() => AccountModel)
@Column @Column
accountId: number accountId: number
@ -174,9 +179,15 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
static loadByUrl (url: string, t?: Sequelize.Transaction) { static loadByUrl (url: string, t?: Sequelize.Transaction) {
const query: IFindOptions<VideoChannelModel> = { const query: IFindOptions<VideoChannelModel> = {
where: { include: [
url {
} model: ActorModel,
required: true,
where: {
url
}
}
]
} }
if (t !== undefined) query.transaction = t if (t !== undefined) query.transaction = t
@ -264,27 +275,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
} }
toActivityPubObject () { toActivityPubObject () {
let sharesObject return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel')
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
}
} }
} }

View File

@ -1,6 +1,6 @@
export interface ActivityPubActor { export interface ActivityPubActor {
'@context': any[] '@context': any[]
type: 'Person' | 'Application' type: 'Person' | 'Application' | 'Group'
id: string id: string
following: string following: string
followers: string followers: string