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