464105261a463bba2e58b03dae10cce89fc5ab29
[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: false,
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     onDelete: 'cascade'
267   })
268 }
269
270 function afterDestroy (account: AccountInstance) {
271   if (account.isOwned()) {
272     return sendDeleteAccount(account, undefined)
273   }
274
275   return undefined
276 }
277
278 toFormattedJSON = function (this: AccountInstance) {
279   let host = CONFIG.WEBSERVER.HOST
280   let score: number
281
282   if (this.Server) {
283     host = this.Server.host
284     score = this.Server.score as number
285   }
286
287   const json = {
288     id: this.id,
289     host,
290     score,
291     name: this.name,
292     createdAt: this.createdAt,
293     updatedAt: this.updatedAt
294   }
295
296   return json
297 }
298
299 toActivityPubObject = function (this: AccountInstance) {
300   const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
301
302   const json = {
303     type,
304     id: this.url,
305     following: this.getFollowingUrl(),
306     followers: this.getFollowersUrl(),
307     inbox: this.inboxUrl,
308     outbox: this.outboxUrl,
309     preferredUsername: this.name,
310     url: this.url,
311     name: this.name,
312     endpoints: {
313       sharedInbox: this.sharedInboxUrl
314     },
315     uuid: this.uuid,
316     publicKey: {
317       id: this.getPublicKeyUrl(),
318       owner: this.url,
319       publicKeyPem: this.publicKey
320     }
321   }
322
323   return activityPubContextify(json)
324 }
325
326 isOwned = function (this: AccountInstance) {
327   return this.serverId === null
328 }
329
330 getFollowerSharedInboxUrls = function (this: AccountInstance) {
331   const query: Sequelize.FindOptions<AccountAttributes> = {
332     attributes: [ 'sharedInboxUrl' ],
333     include: [
334       {
335         model: Account['sequelize'].models.AccountFollow,
336         required: true,
337         as: 'followers',
338         where: {
339           targetAccountId: this.id
340         }
341       }
342     ]
343   }
344
345   return Account.findAll(query)
346     .then(accounts => accounts.map(a => a.sharedInboxUrl))
347 }
348
349 getFollowingUrl = function (this: AccountInstance) {
350   return this.url + '/following'
351 }
352
353 getFollowersUrl = function (this: AccountInstance) {
354   return this.url + '/followers'
355 }
356
357 getPublicKeyUrl = function (this: AccountInstance) {
358   return this.url + '#main-key'
359 }
360
361 // ------------------------------ STATICS ------------------------------
362
363 listOwned = function () {
364   const query: Sequelize.FindOptions<AccountAttributes> = {
365     where: {
366       serverId: null
367     }
368   }
369
370   return Account.findAll(query)
371 }
372
373 loadApplication = function () {
374   return Account.findOne({
375     include: [
376       {
377         model: Account['sequelize'].models.Application,
378         required: true
379       }
380     ]
381   })
382 }
383
384 load = function (id: number) {
385   return Account.findById(id)
386 }
387
388 loadByUUID = function (uuid: string) {
389   const query: Sequelize.FindOptions<AccountAttributes> = {
390     where: {
391       uuid
392     }
393   }
394
395   return Account.findOne(query)
396 }
397
398 loadLocalByName = function (name: string) {
399   const query: Sequelize.FindOptions<AccountAttributes> = {
400     where: {
401       name,
402       [Sequelize.Op.or]: [
403         {
404           userId: {
405             [Sequelize.Op.ne]: null
406           }
407         },
408         {
409           applicationId: {
410             [Sequelize.Op.ne]: null
411           }
412         }
413       ]
414     }
415   }
416
417   return Account.findOne(query)
418 }
419
420 loadByNameAndHost = function (name: string, host: string) {
421   const query: Sequelize.FindOptions<AccountAttributes> = {
422     where: {
423       name
424     },
425     include: [
426       {
427         model: Account['sequelize'].models.Server,
428         required: true,
429         where: {
430           host
431         }
432       }
433     ]
434   }
435
436   return Account.findOne(query)
437 }
438
439 loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
440   const query: Sequelize.FindOptions<AccountAttributes> = {
441     where: {
442       url
443     },
444     transaction
445   }
446
447   return Account.findOne(query)
448 }
449
450 loadAccountByServerAndUUID = function (uuid: string, serverId: number, transaction: Sequelize.Transaction) {
451   const query: Sequelize.FindOptions<AccountAttributes> = {
452     where: {
453       serverId,
454       uuid
455     },
456     transaction
457   }
458
459   return Account.find(query)
460 }