Optimize account creation
[oweals/peertube.git] / server / models / account / account.ts
1 import * as Sequelize from 'sequelize'
2
3 import {
4   isUserUsernameValid,
5   isAccountPublicKeyValid,
6   isAccountUrlValid,
7   isAccountPrivateKeyValid,
8   isAccountFollowersCountValid,
9   isAccountFollowingCountValid,
10   isAccountInboxValid,
11   isAccountOutboxValid,
12   isAccountSharedInboxValid,
13   isAccountFollowersValid,
14   isAccountFollowingValid,
15   activityPubContextify
16 } from '../../helpers'
17
18 import { addMethodsToModel, getSort } from '../utils'
19 import {
20   AccountInstance,
21   AccountAttributes,
22
23   AccountMethods
24 } from './account-interface'
25 import { sendDeleteAccount } from '../../lib/activitypub/send-request'
26 import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants'
27
28 let Account: Sequelize.Model<AccountInstance, AccountAttributes>
29 let loadAccountByServerAndUUID: AccountMethods.LoadAccountByServerAndUUID
30 let load: AccountMethods.Load
31 let loadApplication: AccountMethods.LoadApplication
32 let loadByUUID: AccountMethods.LoadByUUID
33 let loadByUrl: AccountMethods.LoadByUrl
34 let loadLocalByName: AccountMethods.LoadLocalByName
35 let loadByNameAndHost: AccountMethods.LoadByNameAndHost
36 let listOwned: AccountMethods.ListOwned
37 let isOwned: AccountMethods.IsOwned
38 let toActivityPubObject: AccountMethods.ToActivityPubObject
39 let toFormattedJSON: AccountMethods.ToFormattedJSON
40 let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
41 let getFollowingUrl: AccountMethods.GetFollowingUrl
42 let getFollowersUrl: AccountMethods.GetFollowersUrl
43 let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
44
45 export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
46   Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
47     {
48       uuid: {
49         type: DataTypes.UUID,
50         defaultValue: DataTypes.UUIDV4,
51         allowNull: false,
52         validate: {
53           isUUID: 4
54         }
55       },
56       name: {
57         type: DataTypes.STRING,
58         allowNull: false,
59         validate: {
60           nameValid: value => {
61             const res = isUserUsernameValid(value)
62             if (res === false) throw new Error('Name is not valid.')
63           }
64         }
65       },
66       url: {
67         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
68         allowNull: false,
69         validate: {
70           urlValid: value => {
71             const res = isAccountUrlValid(value)
72             if (res === false) throw new Error('URL is not valid.')
73           }
74         }
75       },
76       publicKey: {
77         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max),
78         allowNull: true,
79         validate: {
80           publicKeyValid: value => {
81             const res = isAccountPublicKeyValid(value)
82             if (res === false) throw new Error('Public key is not valid.')
83           }
84         }
85       },
86       privateKey: {
87         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
88         allowNull: true,
89         validate: {
90           privateKeyValid: value => {
91             const res = isAccountPrivateKeyValid(value)
92             if (res === false) throw new Error('Private key is not valid.')
93           }
94         }
95       },
96       followersCount: {
97         type: DataTypes.INTEGER,
98         allowNull: false,
99         validate: {
100           followersCountValid: value => {
101             const res = isAccountFollowersCountValid(value)
102             if (res === false) throw new Error('Followers count is not valid.')
103           }
104         }
105       },
106       followingCount: {
107         type: DataTypes.INTEGER,
108         allowNull: false,
109         validate: {
110           followingCountValid: value => {
111             const res = isAccountFollowingCountValid(value)
112             if (res === false) throw new Error('Following count is not valid.')
113           }
114         }
115       },
116       inboxUrl: {
117         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
118         allowNull: false,
119         validate: {
120           inboxUrlValid: value => {
121             const res = isAccountInboxValid(value)
122             if (res === false) throw new Error('Inbox URL is not valid.')
123           }
124         }
125       },
126       outboxUrl: {
127         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
128         allowNull: false,
129         validate: {
130           outboxUrlValid: value => {
131             const res = isAccountOutboxValid(value)
132             if (res === false) throw new Error('Outbox URL is not valid.')
133           }
134         }
135       },
136       sharedInboxUrl: {
137         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
138         allowNull: false,
139         validate: {
140           sharedInboxUrlValid: value => {
141             const res = isAccountSharedInboxValid(value)
142             if (res === false) throw new Error('Shared inbox URL is not valid.')
143           }
144         }
145       },
146       followersUrl: {
147         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
148         allowNull: false,
149         validate: {
150           followersUrlValid: value => {
151             const res = isAccountFollowersValid(value)
152             if (res === false) throw new Error('Followers URL is not valid.')
153           }
154         }
155       },
156       followingUrl: {
157         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
158         allowNull: false,
159         validate: {
160           followingUrlValid: value => {
161             const res = isAccountFollowingValid(value)
162             if (res === false) throw new Error('Following URL is not valid.')
163           }
164         }
165       }
166     },
167     {
168       indexes: [
169         {
170           fields: [ 'name' ]
171         },
172         {
173           fields: [ 'serverId' ]
174         },
175         {
176           fields: [ 'userId' ],
177           unique: true
178         },
179         {
180           fields: [ 'applicationId' ],
181           unique: true
182         },
183         {
184           fields: [ 'name', 'serverId', 'applicationId' ],
185           unique: true
186         }
187       ],
188       hooks: { afterDestroy }
189     }
190   )
191
192   const classMethods = [
193     associate,
194     loadAccountByServerAndUUID,
195     loadApplication,
196     load,
197     loadByUUID,
198     loadByUrl,
199     loadLocalByName,
200     loadByNameAndHost,
201     listOwned
202   ]
203   const instanceMethods = [
204     isOwned,
205     toActivityPubObject,
206     toFormattedJSON,
207     getFollowerSharedInboxUrls,
208     getFollowingUrl,
209     getFollowersUrl,
210     getPublicKeyUrl
211   ]
212   addMethodsToModel(Account, classMethods, instanceMethods)
213
214   return Account
215 }
216
217 // ---------------------------------------------------------------------------
218
219 function associate (models) {
220   Account.belongsTo(models.Server, {
221     foreignKey: {
222       name: 'serverId',
223       allowNull: true
224     },
225     onDelete: 'cascade'
226   })
227
228   Account.belongsTo(models.User, {
229     foreignKey: {
230       name: 'userId',
231       allowNull: true
232     },
233     onDelete: 'cascade'
234   })
235
236   Account.belongsTo(models.Application, {
237     foreignKey: {
238       name: 'applicationId',
239       allowNull: true
240     },
241     onDelete: 'cascade'
242   })
243
244   Account.hasMany(models.VideoChannel, {
245     foreignKey: {
246       name: 'accountId',
247       allowNull: false
248     },
249     onDelete: 'cascade',
250     hooks: true
251   })
252
253   Account.hasMany(models.AccountFollow, {
254     foreignKey: {
255       name: 'accountId',
256       allowNull: false
257     },
258     onDelete: 'cascade'
259   })
260
261   Account.hasMany(models.AccountFollow, {
262     foreignKey: {
263       name: 'targetAccountId',
264       allowNull: false
265     },
266     as: 'followers',
267     onDelete: 'cascade'
268   })
269 }
270
271 function afterDestroy (account: AccountInstance) {
272   if (account.isOwned()) {
273     return sendDeleteAccount(account, undefined)
274   }
275
276   return undefined
277 }
278
279 toFormattedJSON = function (this: AccountInstance) {
280   let host = CONFIG.WEBSERVER.HOST
281   let score: number
282
283   if (this.Server) {
284     host = this.Server.host
285     score = this.Server.score as number
286   }
287
288   const json = {
289     id: this.id,
290     host,
291     score,
292     name: this.name,
293     createdAt: this.createdAt,
294     updatedAt: this.updatedAt
295   }
296
297   return json
298 }
299
300 toActivityPubObject = function (this: AccountInstance) {
301   const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
302
303   const json = {
304     type,
305     id: this.url,
306     following: this.getFollowingUrl(),
307     followers: this.getFollowersUrl(),
308     inbox: this.inboxUrl,
309     outbox: this.outboxUrl,
310     preferredUsername: this.name,
311     url: this.url,
312     name: this.name,
313     endpoints: {
314       sharedInbox: this.sharedInboxUrl
315     },
316     uuid: this.uuid,
317     publicKey: {
318       id: this.getPublicKeyUrl(),
319       owner: this.url,
320       publicKeyPem: this.publicKey
321     }
322   }
323
324   return activityPubContextify(json)
325 }
326
327 isOwned = function (this: AccountInstance) {
328   return this.serverId === null
329 }
330
331 getFollowerSharedInboxUrls = function (this: AccountInstance) {
332   const query: Sequelize.FindOptions<AccountAttributes> = {
333     attributes: [ 'sharedInboxUrl' ],
334     include: [
335       {
336         model: Account['sequelize'].models.AccountFollow,
337         required: true,
338         as: 'followers',
339         where: {
340           targetAccountId: this.id
341         }
342       }
343     ]
344   }
345
346   return Account.findAll(query)
347     .then(accounts => accounts.map(a => a.sharedInboxUrl))
348 }
349
350 getFollowingUrl = function (this: AccountInstance) {
351   return this.url + '/following'
352 }
353
354 getFollowersUrl = function (this: AccountInstance) {
355   return this.url + '/followers'
356 }
357
358 getPublicKeyUrl = function (this: AccountInstance) {
359   return this.url + '#main-key'
360 }
361
362 // ------------------------------ STATICS ------------------------------
363
364 listOwned = function () {
365   const query: Sequelize.FindOptions<AccountAttributes> = {
366     where: {
367       serverId: null
368     }
369   }
370
371   return Account.findAll(query)
372 }
373
374 loadApplication = function () {
375   return Account.findOne({
376     include: [
377       {
378         model: Account['sequelize'].models.Application,
379         required: true
380       }
381     ]
382   })
383 }
384
385 load = function (id: number) {
386   return Account.findById(id)
387 }
388
389 loadByUUID = function (uuid: string) {
390   const query: Sequelize.FindOptions<AccountAttributes> = {
391     where: {
392       uuid
393     }
394   }
395
396   return Account.findOne(query)
397 }
398
399 loadLocalByName = function (name: string) {
400   const query: Sequelize.FindOptions<AccountAttributes> = {
401     where: {
402       name,
403       [Sequelize.Op.or]: [
404         {
405           userId: {
406             [Sequelize.Op.ne]: null
407           }
408         },
409         {
410           applicationId: {
411             [Sequelize.Op.ne]: null
412           }
413         }
414       ]
415     }
416   }
417
418   return Account.findOne(query)
419 }
420
421 loadByNameAndHost = function (name: string, host: string) {
422   const query: Sequelize.FindOptions<AccountAttributes> = {
423     where: {
424       name
425     },
426     include: [
427       {
428         model: Account['sequelize'].models.Server,
429         required: true,
430         where: {
431           host
432         }
433       }
434     ]
435   }
436
437   return Account.findOne(query)
438 }
439
440 loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
441   const query: Sequelize.FindOptions<AccountAttributes> = {
442     where: {
443       url
444     },
445     transaction
446   }
447
448   return Account.findOne(query)
449 }
450
451 loadAccountByServerAndUUID = function (uuid: string, serverId: number, transaction: Sequelize.Transaction) {
452   const query: Sequelize.FindOptions<AccountAttributes> = {
453     where: {
454       serverId,
455       uuid
456     },
457     transaction
458   }
459
460   return Account.find(query)
461 }