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