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