aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/outbox.ts4
-rw-r--r--server/controllers/api/server/follows.ts2
-rw-r--r--server/controllers/api/videos/channel.ts11
-rw-r--r--server/controllers/webfinger.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/account.ts92
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts91
-rw-r--r--server/helpers/custom-validators/activitypub/index.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/undo.ts2
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/user.ts24
-rw-r--r--server/models/account/account.ts234
-rw-r--r--server/models/activitypub/actor.ts245
-rw-r--r--server/models/video/video-channel.ts60
14 files changed, 425 insertions, 350 deletions
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
40 // This is a shared video 40 // This is a shared video
41 const videoChannel = video.VideoChannel 41 const videoChannel = video.VideoChannel
42 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { 42 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
43 const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.url, videoObject, undefined) 43 const addActivity = await addActivityData(video.url, videoChannel.Account, video, videoChannel.Actor.url, videoObject, undefined)
44 44
45 const url = getAnnounceActivityPubUrl(video.url, account) 45 const url = getAnnounceActivityPubUrl(video.url, account)
46 const announceActivity = await announceActivityData(url, account, addActivity, undefined) 46 const announceActivity = await announceActivityData(url, account, addActivity, undefined)
47 47
48 activities.push(announceActivity) 48 activities.push(announceActivity)
49 } else { 49 } else {
50 const addActivity = await addActivityData(video.url, account, video, videoChannel.url, videoObject, undefined) 50 const addActivity = await addActivityData(video.url, account, video, videoChannel.Actor.url, videoObject, undefined)
51 51
52 activities.push(addActivity) 52 activities.push(addActivity)
53 } 53 }
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:
157 // This could be long so don't wait this task 157 // This could be long so don't wait this task
158 const following = follow.AccountFollowing 158 const following = follow.AccountFollowing
159 following.destroy() 159 following.destroy()
160 .catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.url, err)) 160 .catch(err => logger.error('Cannot destroy account that we do not follow anymore %s.', following.Actor.url, err))
161 161
162 return res.status(204).end() 162 return res.status(204).end()
163} 163}
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
92 return res.type('json').status(204).end() 92 return res.type('json').status(204).end()
93} 93}
94 94
95async function addVideoChannel (req: express.Request, res: express.Response) { 95function addVideoChannel (req: express.Request, res: express.Response) {
96 const videoChannelInfo: VideoChannelCreate = req.body 96 const videoChannelInfo: VideoChannelCreate = req.body
97 const account: AccountModel = res.locals.oauth.token.User.Account 97 const account: AccountModel = res.locals.oauth.token.User.Account
98 let videoChannelCreated: VideoChannelModel
99 98
100 await sequelizeTypescript.transaction(async t => { 99 return sequelizeTypescript.transaction(async t => {
101 videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t) 100 const videoChannelCreated = await createVideoChannel(videoChannelInfo, account, t)
102 })
103 101
104 logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid) 102 logger.info('Video channel with uuid %s created.', videoChannelCreated.uuid)
103 })
105} 104}
106 105
107async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 106async 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:
23 23
24 const json = { 24 const json = {
25 subject: req.query.resource, 25 subject: req.query.resource,
26 aliases: [ account.url ], 26 aliases: [ account.Actor.url ],
27 links: [ 27 links: [
28 { 28 {
29 rel: 'self', 29 rel: 'self',
30 type: 'application/activity+json', 30 type: 'application/activity+json',
31 href: account.url 31 href: account.Actor.url
32 } 32 }
33 ] 33 ]
34 } 34 }
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 @@
1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers'
3import { isAccountNameValid } from '../accounts'
4import { exists, isUUIDValid } from '../misc'
5import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
6
7function isAccountEndpointsObjectValid (endpointObject: any) {
8 return isActivityPubUrlValid(endpointObject.sharedInbox)
9}
10
11function isAccountPublicKeyObjectValid (publicKeyObject: any) {
12 return isActivityPubUrlValid(publicKeyObject.id) &&
13 isActivityPubUrlValid(publicKeyObject.owner) &&
14 isAccountPublicKeyValid(publicKeyObject.publicKeyPem)
15}
16
17function isAccountTypeValid (type: string) {
18 return type === 'Person' || type === 'Application'
19}
20
21function isAccountPublicKeyValid (publicKey: string) {
22 return exists(publicKey) &&
23 typeof publicKey === 'string' &&
24 publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
25 publicKey.endsWith('-----END PUBLIC KEY-----') &&
26 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY)
27}
28
29function isAccountPreferredUsernameValid (preferredUsername: string) {
30 return isAccountNameValid(preferredUsername)
31}
32
33function isAccountPrivateKeyValid (privateKey: string) {
34 return exists(privateKey) &&
35 typeof privateKey === 'string' &&
36 privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
37 privateKey.endsWith('-----END RSA PRIVATE KEY-----') &&
38 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY)
39}
40
41function isRemoteAccountValid (remoteAccount: any) {
42 return isActivityPubUrlValid(remoteAccount.id) &&
43 isUUIDValid(remoteAccount.uuid) &&
44 isAccountTypeValid(remoteAccount.type) &&
45 isActivityPubUrlValid(remoteAccount.following) &&
46 isActivityPubUrlValid(remoteAccount.followers) &&
47 isActivityPubUrlValid(remoteAccount.inbox) &&
48 isActivityPubUrlValid(remoteAccount.outbox) &&
49 isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
50 isActivityPubUrlValid(remoteAccount.url) &&
51 isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
52 isAccountEndpointsObjectValid(remoteAccount.endpoints)
53}
54
55function isAccountFollowingCountValid (value: string) {
56 return exists(value) && validator.isInt('' + value, { min: 0 })
57}
58
59function isAccountFollowersCountValid (value: string) {
60 return exists(value) && validator.isInt('' + value, { min: 0 })
61}
62
63function isAccountDeleteActivityValid (activity: any) {
64 return isBaseActivityValid(activity, 'Delete')
65}
66
67function isAccountFollowActivityValid (activity: any) {
68 return isBaseActivityValid(activity, 'Follow') &&
69 isActivityPubUrlValid(activity.object)
70}
71
72function isAccountAcceptActivityValid (activity: any) {
73 return isBaseActivityValid(activity, 'Accept')
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 isAccountEndpointsObjectValid,
80 isAccountPublicKeyObjectValid,
81 isAccountTypeValid,
82 isAccountPublicKeyValid,
83 isAccountPreferredUsernameValid,
84 isAccountPrivateKeyValid,
85 isRemoteAccountValid,
86 isAccountFollowingCountValid,
87 isAccountFollowersCountValid,
88 isAccountNameValid,
89 isAccountFollowActivityValid,
90 isAccountAcceptActivityValid,
91 isAccountDeleteActivityValid
92}
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 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' 3import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './actor'
4import { isAnnounceActivityValid } from './announce' 4import { isAnnounceActivityValid } from './announce'
5import { isActivityPubUrlValid } from './misc' 5import { isActivityPubUrlValid } from './misc'
6import { isDislikeActivityValid, isLikeActivityValid } from './rate' 6import { 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 @@
1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers'
3import { isAccountNameValid } from '../accounts'
4import { exists, isUUIDValid } from '../misc'
5import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
6
7function isActorEndpointsObjectValid (endpointObject: any) {
8 return isActivityPubUrlValid(endpointObject.sharedInbox)
9}
10
11function isActorPublicKeyObjectValid (publicKeyObject: any) {
12 return isActivityPubUrlValid(publicKeyObject.id) &&
13 isActivityPubUrlValid(publicKeyObject.owner) &&
14 isActorPublicKeyValid(publicKeyObject.publicKeyPem)
15}
16
17function isActorTypeValid (type: string) {
18 return type === 'Person' || type === 'Application' || type === 'Group'
19}
20
21function isActorPublicKeyValid (publicKey: string) {
22 return exists(publicKey) &&
23 typeof publicKey === 'string' &&
24 publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
25 publicKey.endsWith('-----END PUBLIC KEY-----') &&
26 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY)
27}
28
29function isActorPreferredUsernameValid (preferredUsername: string) {
30 return isAccountNameValid(preferredUsername)
31}
32
33function isActorPrivateKeyValid (privateKey: string) {
34 return exists(privateKey) &&
35 typeof privateKey === 'string' &&
36 privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
37 privateKey.endsWith('-----END RSA PRIVATE KEY-----') &&
38 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY)
39}
40
41function isRemoteActorValid (remoteActor: any) {
42 return isActivityPubUrlValid(remoteActor.id) &&
43 isUUIDValid(remoteActor.uuid) &&
44 isActorTypeValid(remoteActor.type) &&
45 isActivityPubUrlValid(remoteActor.following) &&
46 isActivityPubUrlValid(remoteActor.followers) &&
47 isActivityPubUrlValid(remoteActor.inbox) &&
48 isActivityPubUrlValid(remoteActor.outbox) &&
49 isActorPreferredUsernameValid(remoteActor.preferredUsername) &&
50 isActivityPubUrlValid(remoteActor.url) &&
51 isActorPublicKeyObjectValid(remoteActor.publicKey) &&
52 isActorEndpointsObjectValid(remoteActor.endpoints)
53}
54
55function isActorFollowingCountValid (value: string) {
56 return exists(value) && validator.isInt('' + value, { min: 0 })
57}
58
59function isActorFollowersCountValid (value: string) {
60 return exists(value) && validator.isInt('' + value, { min: 0 })
61}
62
63function isActorDeleteActivityValid (activity: any) {
64 return isBaseActivityValid(activity, 'Delete')
65}
66
67function isActorFollowActivityValid (activity: any) {
68 return isBaseActivityValid(activity, 'Follow') &&
69 isActivityPubUrlValid(activity.object)
70}
71
72function isActorAcceptActivityValid (activity: any) {
73 return isBaseActivityValid(activity, 'Accept')
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 isActorEndpointsObjectValid,
80 isActorPublicKeyObjectValid,
81 isActorTypeValid,
82 isActorPublicKeyValid,
83 isActorPreferredUsernameValid,
84 isActorPrivateKeyValid,
85 isRemoteActorValid,
86 isActorFollowingCountValid,
87 isActorFollowersCountValid,
88 isActorFollowActivityValid,
89 isActorAcceptActivityValid,
90 isActorDeleteActivityValid
91}
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 @@
1export * from './account' 1export * from './actor'
2export * from './activity' 2export * from './activity'
3export * from './misc' 3export * from './misc'
4export * from './signature' 4export * 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 @@
1import { isAccountFollowActivityValid } from './account' 1import { isAccountFollowActivityValid } from './actor'
2import { isBaseActivityValid } from './misc' 2import { isBaseActivityValid } from './misc'
3import { isDislikeActivityValid, isLikeActivityValid } from './rate' 3import { isDislikeActivityValid, isLikeActivityValid } from './rate'
4 4
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 = {
131 FILE_SIZE: { min: 10 }, 131 FILE_SIZE: { min: 10 },
132 URL: { min: 3, max: 2000 } // Length 132 URL: { min: 3, max: 2000 } // Length
133 }, 133 },
134 ACCOUNTS: { 134 ACTOR: {
135 PUBLIC_KEY: { min: 10, max: 5000 }, // Length 135 PUBLIC_KEY: { min: 10, max: 5000 }, // Length
136 PRIVATE_KEY: { min: 10, max: 5000 }, // Length 136 PRIVATE_KEY: { min: 10, max: 5000 }, // Length
137 URL: { min: 3, max: 2000 } // Length 137 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'
3import { CONFIG, sequelizeTypescript } from '../initializers' 3import { CONFIG, sequelizeTypescript } from '../initializers'
4import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { ActorModel } from '../models/activitypub/actor'
6import { getAccountActivityPubUrl } from './activitypub' 7import { getAccountActivityPubUrl } from './activitypub'
7import { createVideoChannel } from './video-channel' 8import { createVideoChannel } from './video-channel'
8 9
@@ -27,9 +28,10 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true
27 28
28 // Set account keys, this could be long so process after the account creation and do not block the client 29 // Set account keys, this could be long so process after the account creation and do not block the client
29 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 30 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
30 account.set('publicKey', publicKey) 31 const actor = account.Actor
31 account.set('privateKey', privateKey) 32 actor.set('publicKey', publicKey)
32 account.save().catch(err => logger.error('Cannot set public/private keys of local account %d.', account.id, err)) 33 actor.set('privateKey', privateKey)
34 actor.save().catch(err => logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err))
33 35
34 return { account, videoChannel } 36 return { account, videoChannel }
35} 37}
@@ -37,8 +39,7 @@ async function createUserAccountAndChannel (user: UserModel, validateUser = true
37async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) { 39async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) {
38 const url = getAccountActivityPubUrl(name) 40 const url = getAccountActivityPubUrl(name)
39 41
40 const accountInstance = new AccountModel({ 42 const actorInstance = new ActorModel({
41 name,
42 url, 43 url,
43 publicKey: null, 44 publicKey: null,
44 privateKey: null, 45 privateKey: null,
@@ -48,13 +49,22 @@ async function createLocalAccountWithoutKeys (name: string, userId: number, appl
48 outboxUrl: url + '/outbox', 49 outboxUrl: url + '/outbox',
49 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', 50 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
50 followersUrl: url + '/followers', 51 followersUrl: url + '/followers',
51 followingUrl: url + '/following', 52 followingUrl: url + '/following'
53 })
54 const actorInstanceCreated = await actorInstance.save({ transaction: t })
55
56 const accountInstance = new AccountModel({
57 name,
52 userId, 58 userId,
53 applicationId, 59 applicationId,
60 actorId: actorInstanceCreated.id,
54 serverId: null // It is our server 61 serverId: null // It is our server
55 }) 62 })
56 63
57 return accountInstance.save({ transaction: t }) 64 const accountInstanceCreated = await accountInstance.save({ transaction: t })
65 accountInstanceCreated.Actor = actorInstanceCreated
66
67 return accountInstanceCreated
58} 68}
59 69
60// --------------------------------------------------------------------------- 70// ---------------------------------------------------------------------------
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 @@
1import { join } from 'path'
2import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
3import { 2import {
4 AfterDestroy, 3 AfterDestroy,
@@ -16,24 +15,13 @@ import {
16 Table, 15 Table,
17 UpdatedAt 16 UpdatedAt
18} from 'sequelize-typescript' 17} from 'sequelize-typescript'
19import { Avatar } from '../../../shared/models/avatars/avatar.model'
20import { activityPubContextify } from '../../helpers'
21import {
22 isAccountFollowersCountValid,
23 isAccountFollowingCountValid,
24 isAccountPrivateKeyValid,
25 isAccountPublicKeyValid,
26 isActivityPubUrlValid
27} from '../../helpers/custom-validators/activitypub'
28import { isUserUsernameValid } from '../../helpers/custom-validators/users' 18import { isUserUsernameValid } from '../../helpers/custom-validators/users'
29import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
30import { sendDeleteAccount } from '../../lib/activitypub/send' 19import { sendDeleteAccount } from '../../lib/activitypub/send'
20import { ActorModel } from '../activitypub/actor'
31import { ApplicationModel } from '../application/application' 21import { ApplicationModel } from '../application/application'
32import { AvatarModel } from '../avatar/avatar'
33import { ServerModel } from '../server/server' 22import { ServerModel } from '../server/server'
34import { throwIfNotValid } from '../utils' 23import { throwIfNotValid } from '../utils'
35import { VideoChannelModel } from '../video/video-channel' 24import { VideoChannelModel } from '../video/video-channel'
36import { AccountFollowModel } from './account-follow'
37import { UserModel } from './user' 25import { UserModel } from './user'
38 26
39@Table({ 27@Table({
@@ -59,68 +47,7 @@ import { UserModel } from './user'
59 } 47 }
60 ] 48 ]
61}) 49})
62export class AccountModel extends Model<Account> { 50export class AccountModel extends Model<AccountModel> {
63
64 @AllowNull(false)
65 @Default(DataType.UUIDV4)
66 @IsUUID(4)
67 @Column(DataType.UUID)
68 uuid: string
69
70 @AllowNull(false)
71 @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
72 @Column
73 name: string
74
75 @AllowNull(false)
76 @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
77 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
78 url: string
79
80 @AllowNull(true)
81 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key'))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max))
83 publicKey: string
84
85 @AllowNull(true)
86 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key'))
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max))
88 privateKey: string
89
90 @AllowNull(false)
91 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count'))
92 @Column
93 followersCount: number
94
95 @AllowNull(false)
96 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count'))
97 @Column
98 followingCount: number
99
100 @AllowNull(false)
101 @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
102 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
103 inboxUrl: string
104
105 @AllowNull(false)
106 @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
107 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
108 outboxUrl: string
109
110 @AllowNull(false)
111 @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
112 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
113 sharedInboxUrl: string
114
115 @AllowNull(false)
116 @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
117 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
118 followersUrl: string
119
120 @AllowNull(false)
121 @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
122 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
123 followingUrl: string
124 51
125 @CreatedAt 52 @CreatedAt
126 createdAt: Date 53 createdAt: Date
@@ -128,29 +55,17 @@ export class AccountModel extends Model<Account> {
128 @UpdatedAt 55 @UpdatedAt
129 updatedAt: Date 56 updatedAt: Date
130 57
131 @ForeignKey(() => AvatarModel) 58 @ForeignKey(() => ActorModel)
132 @Column
133 avatarId: number
134
135 @BelongsTo(() => AvatarModel, {
136 foreignKey: {
137 allowNull: true
138 },
139 onDelete: 'cascade'
140 })
141 Avatar: AvatarModel
142
143 @ForeignKey(() => ServerModel)
144 @Column 59 @Column
145 serverId: number 60 actorId: number
146 61
147 @BelongsTo(() => ServerModel, { 62 @BelongsTo(() => ActorModel, {
148 foreignKey: { 63 foreignKey: {
149 allowNull: true 64 allowNull: false
150 }, 65 },
151 onDelete: 'cascade' 66 onDelete: 'cascade'
152 }) 67 })
153 Server: ServerModel 68 Actor: ActorModel
154 69
155 @ForeignKey(() => UserModel) 70 @ForeignKey(() => UserModel)
156 @Column 71 @Column
@@ -185,25 +100,6 @@ export class AccountModel extends Model<Account> {
185 }) 100 })
186 VideoChannels: VideoChannelModel[] 101 VideoChannels: VideoChannelModel[]
187 102
188 @HasMany(() => AccountFollowModel, {
189 foreignKey: {
190 name: 'accountId',
191 allowNull: false
192 },
193 onDelete: 'cascade'
194 })
195 AccountFollowing: AccountFollowModel[]
196
197 @HasMany(() => AccountFollowModel, {
198 foreignKey: {
199 name: 'targetAccountId',
200 allowNull: false
201 },
202 as: 'followers',
203 onDelete: 'cascade'
204 })
205 AccountFollowers: AccountFollowModel[]
206
207 @AfterDestroy 103 @AfterDestroy
208 static sendDeleteIfOwned (instance: AccountModel) { 104 static sendDeleteIfOwned (instance: AccountModel) {
209 if (instance.isOwned()) { 105 if (instance.isOwned()) {
@@ -281,9 +177,15 @@ export class AccountModel extends Model<Account> {
281 177
282 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 178 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
283 const query = { 179 const query = {
284 where: { 180 include: [
285 url 181 {
286 }, 182 model: ActorModel,
183 required: true,
184 where: {
185 url
186 }
187 }
188 ],
287 transaction 189 transaction
288 } 190 }
289 191
@@ -292,11 +194,17 @@ export class AccountModel extends Model<Account> {
292 194
293 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { 195 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
294 const query = { 196 const query = {
295 where: { 197 include: [
296 followersUrl: { 198 {
297 [ Sequelize.Op.in ]: followersUrls 199 model: ActorModel,
200 required: true,
201 where: {
202 followersUrl: {
203 [ Sequelize.Op.in ]: followersUrls
204 }
205 }
298 } 206 }
299 }, 207 ],
300 transaction 208 transaction
301 } 209 }
302 210
@@ -304,97 +212,21 @@ export class AccountModel extends Model<Account> {
304 } 212 }
305 213
306 toFormattedJSON () { 214 toFormattedJSON () {
307 let host = CONFIG.WEBSERVER.HOST 215 const actor = this.Actor.toFormattedJSON()
308 let score: number 216 const account = {
309 let avatar: Avatar = null
310
311 if (this.Avatar) {
312 avatar = {
313 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
314 createdAt: this.Avatar.createdAt,
315 updatedAt: this.Avatar.updatedAt
316 }
317 }
318
319 if (this.Server) {
320 host = this.Server.host
321 score = this.Server.score
322 }
323
324 return {
325 id: this.id, 217 id: this.id,
326 uuid: this.uuid,
327 host,
328 score,
329 name: this.name,
330 followingCount: this.followingCount,
331 followersCount: this.followersCount,
332 createdAt: this.createdAt, 218 createdAt: this.createdAt,
333 updatedAt: this.updatedAt, 219 updatedAt: this.updatedAt
334 avatar
335 } 220 }
221
222 return Object.assign(actor, account)
336 } 223 }
337 224
338 toActivityPubObject () { 225 toActivityPubObject () {
339 const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' 226 return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account')
340
341 const json = {
342 type,
343 id: this.url,
344 following: this.getFollowingUrl(),
345 followers: this.getFollowersUrl(),
346 inbox: this.inboxUrl,
347 outbox: this.outboxUrl,
348 preferredUsername: this.name,
349 url: this.url,
350 name: this.name,
351 endpoints: {
352 sharedInbox: this.sharedInboxUrl
353 },
354 uuid: this.uuid,
355 publicKey: {
356 id: this.getPublicKeyUrl(),
357 owner: this.url,
358 publicKeyPem: this.publicKey
359 }
360 }
361
362 return activityPubContextify(json)
363 } 227 }
364 228
365 isOwned () { 229 isOwned () {
366 return this.serverId === null 230 return this.Actor.isOwned()
367 }
368
369 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
370 const query = {
371 attributes: [ 'sharedInboxUrl' ],
372 include: [
373 {
374 model: AccountFollowModel,
375 required: true,
376 as: 'followers',
377 where: {
378 targetAccountId: this.id
379 }
380 }
381 ],
382 transaction: t
383 }
384
385 return AccountModel.findAll(query)
386 .then(accounts => accounts.map(a => a.sharedInboxUrl))
387 }
388
389 getFollowingUrl () {
390 return this.url + '/following'
391 }
392
393 getFollowersUrl () {
394 return this.url + '/followers'
395 }
396
397 getPublicKeyUrl () {
398 return this.url + '#main-key'
399 } 231 }
400} 232}
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 @@
1import { join } from 'path'
2import * as Sequelize from 'sequelize'
3import {
4 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table,
5 UpdatedAt
6} from 'sequelize-typescript'
7import { Avatar } from '../../../shared/models/avatars/avatar.model'
8import { activityPubContextify } from '../../helpers'
9import {
10 isActivityPubUrlValid,
11 isActorFollowersCountValid,
12 isActorFollowingCountValid, isActorPreferredUsernameValid,
13 isActorPrivateKeyValid,
14 isActorPublicKeyValid
15} from '../../helpers/custom-validators/activitypub'
16import { isUserUsernameValid } from '../../helpers/custom-validators/users'
17import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
18import { AccountFollowModel } from '../account/account-follow'
19import { AvatarModel } from '../avatar/avatar'
20import { ServerModel } from '../server/server'
21import { throwIfNotValid } from '../utils'
22
23@Table({
24 tableName: 'actor'
25})
26export class ActorModel extends Model<ActorModel> {
27
28 @AllowNull(false)
29 @Default(DataType.UUIDV4)
30 @IsUUID(4)
31 @Column(DataType.UUID)
32 uuid: string
33
34 @AllowNull(false)
35 @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name'))
36 @Column
37 name: string
38
39 @AllowNull(false)
40 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
41 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
42 url: string
43
44 @AllowNull(true)
45 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key'))
46 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY.max))
47 publicKey: string
48
49 @AllowNull(true)
50 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key'))
51 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY.max))
52 privateKey: string
53
54 @AllowNull(false)
55 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
56 @Column
57 followersCount: number
58
59 @AllowNull(false)
60 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
61 @Column
62 followingCount: number
63
64 @AllowNull(false)
65 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
66 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
67 inboxUrl: string
68
69 @AllowNull(false)
70 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
71 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
72 outboxUrl: string
73
74 @AllowNull(false)
75 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
76 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
77 sharedInboxUrl: string
78
79 @AllowNull(false)
80 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
81 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
82 followersUrl: string
83
84 @AllowNull(false)
85 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
86 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
87 followingUrl: string
88
89 @CreatedAt
90 createdAt: Date
91
92 @UpdatedAt
93 updatedAt: Date
94
95 @ForeignKey(() => AvatarModel)
96 @Column
97 avatarId: number
98
99 @BelongsTo(() => AvatarModel, {
100 foreignKey: {
101 allowNull: true
102 },
103 onDelete: 'cascade'
104 })
105 Avatar: AvatarModel
106
107 @HasMany(() => AccountFollowModel, {
108 foreignKey: {
109 name: 'accountId',
110 allowNull: false
111 },
112 onDelete: 'cascade'
113 })
114 AccountFollowing: AccountFollowModel[]
115
116 @HasMany(() => AccountFollowModel, {
117 foreignKey: {
118 name: 'targetAccountId',
119 allowNull: false
120 },
121 as: 'followers',
122 onDelete: 'cascade'
123 })
124 AccountFollowers: AccountFollowModel[]
125
126 @ForeignKey(() => ServerModel)
127 @Column
128 serverId: number
129
130 @BelongsTo(() => ServerModel, {
131 foreignKey: {
132 allowNull: true
133 },
134 onDelete: 'cascade'
135 })
136 Server: ServerModel
137
138 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
139 const query = {
140 where: {
141 followersUrl: {
142 [ Sequelize.Op.in ]: followersUrls
143 }
144 },
145 transaction
146 }
147
148 return ActorModel.findAll(query)
149 }
150
151 toFormattedJSON () {
152 let avatar: Avatar = null
153 if (this.Avatar) {
154 avatar = {
155 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
156 createdAt: this.Avatar.createdAt,
157 updatedAt: this.Avatar.updatedAt
158 }
159 }
160
161 let host = CONFIG.WEBSERVER.HOST
162 let score: number
163 if (this.Server) {
164 host = this.Server.host
165 score = this.Server.score
166 }
167
168 return {
169 id: this.id,
170 host,
171 score,
172 followingCount: this.followingCount,
173 followersCount: this.followersCount,
174 avatar
175 }
176 }
177
178 toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') {
179 let activityPubType
180 if (type === 'Account') {
181 activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
182 } else { // VideoChannel
183 activityPubType = 'Group'
184 }
185
186 const json = {
187 type,
188 id: this.url,
189 following: this.getFollowingUrl(),
190 followers: this.getFollowersUrl(),
191 inbox: this.inboxUrl,
192 outbox: this.outboxUrl,
193 preferredUsername: name,
194 url: this.url,
195 name,
196 endpoints: {
197 sharedInbox: this.sharedInboxUrl
198 },
199 uuid,
200 publicKey: {
201 id: this.getPublicKeyUrl(),
202 owner: this.url,
203 publicKeyPem: this.publicKey
204 }
205 }
206
207 return activityPubContextify(json)
208 }
209
210 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
211 const query = {
212 attributes: [ 'sharedInboxUrl' ],
213 include: [
214 {
215 model: AccountFollowModel,
216 required: true,
217 as: 'followers',
218 where: {
219 targetAccountId: this.id
220 }
221 }
222 ],
223 transaction: t
224 }
225
226 return ActorModel.findAll(query)
227 .then(accounts => accounts.map(a => a.sharedInboxUrl))
228 }
229
230 getFollowingUrl () {
231 return this.url + '/following'
232 }
233
234 getFollowersUrl () {
235 return this.url + '/followers'
236 }
237
238 getPublicKeyUrl () {
239 return this.url + '#main-key'
240 }
241
242 isOwned () {
243 return this.serverId === null
244 }
245}
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 {
11 HasMany, 11 HasMany,
12 Is, 12 Is,
13 IsUUID, 13 IsUUID,
14 Model, Scopes, 14 Model,
15 Scopes,
15 Table, 16 Table,
16 UpdatedAt 17 UpdatedAt
17} from 'sequelize-typescript' 18} from 'sequelize-typescript'
18import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' 19import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
19import { activityPubCollection } from '../../helpers'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
21import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' 20import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
22import { CONSTRAINTS_FIELDS } from '../../initializers'
23import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
24import { sendDeleteVideoChannel } from '../../lib/activitypub/send' 21import { sendDeleteVideoChannel } from '../../lib/activitypub/send'
25import { AccountModel } from '../account/account' 22import { AccountModel } from '../account/account'
23import { ActorModel } from '../activitypub/actor'
26import { ServerModel } from '../server/server' 24import { ServerModel } from '../server/server'
27import { getSort, throwIfNotValid } from '../utils' 25import { getSort, throwIfNotValid } from '../utils'
28import { VideoModel } from './video' 26import { VideoModel } from './video'
@@ -78,17 +76,24 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
78 @Column 76 @Column
79 remote: boolean 77 remote: boolean
80 78
81 @AllowNull(false)
82 @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
83 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max))
84 url: string
85
86 @CreatedAt 79 @CreatedAt
87 createdAt: Date 80 createdAt: Date
88 81
89 @UpdatedAt 82 @UpdatedAt
90 updatedAt: Date 83 updatedAt: Date
91 84
85 @ForeignKey(() => ActorModel)
86 @Column
87 actorId: number
88
89 @BelongsTo(() => ActorModel, {
90 foreignKey: {
91 allowNull: false
92 },
93 onDelete: 'cascade'
94 })
95 Actor: ActorModel
96
92 @ForeignKey(() => AccountModel) 97 @ForeignKey(() => AccountModel)
93 @Column 98 @Column
94 accountId: number 99 accountId: number
@@ -174,9 +179,15 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
174 179
175 static loadByUrl (url: string, t?: Sequelize.Transaction) { 180 static loadByUrl (url: string, t?: Sequelize.Transaction) {
176 const query: IFindOptions<VideoChannelModel> = { 181 const query: IFindOptions<VideoChannelModel> = {
177 where: { 182 include: [
178 url 183 {
179 } 184 model: ActorModel,
185 required: true,
186 where: {
187 url
188 }
189 }
190 ]
180 } 191 }
181 192
182 if (t !== undefined) query.transaction = t 193 if (t !== undefined) query.transaction = t
@@ -264,27 +275,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
264 } 275 }
265 276
266 toActivityPubObject () { 277 toActivityPubObject () {
267 let sharesObject 278 return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel')
268 if (Array.isArray(this.VideoChannelShares)) {
269 const shares: string[] = []
270
271 for (const videoChannelShare of this.VideoChannelShares) {
272 const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account)
273 shares.push(shareUrl)
274 }
275
276 sharesObject = activityPubCollection(shares)
277 }
278
279 return {
280 type: 'VideoChannel' as 'VideoChannel',
281 id: this.url,
282 uuid: this.uuid,
283 content: this.description,
284 name: this.name,
285 published: this.createdAt.toISOString(),
286 updated: this.updatedAt.toISOString(),
287 shares: sharesObject
288 }
289 } 279 }
290} 280}