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