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