Type toFormattedJSON
[oweals/peertube.git] / server / models / activitypub / actor.ts
1 import { values } from 'lodash'
2 import { extname } from 'path'
3 import * as Sequelize from 'sequelize'
4 import {
5   AllowNull,
6   BelongsTo,
7   Column,
8   CreatedAt,
9   DataType,
10   DefaultScope,
11   ForeignKey,
12   HasMany,
13   HasOne,
14   Is,
15   Model,
16   Scopes,
17   Table,
18   UpdatedAt
19 } from 'sequelize-typescript'
20 import { ActivityPubActorType } from '../../../shared/models/activitypub'
21 import { Avatar } from '../../../shared/models/avatars/avatar.model'
22 import { activityPubContextify } from '../../helpers/activitypub'
23 import {
24   isActorFollowersCountValid,
25   isActorFollowingCountValid,
26   isActorPreferredUsernameValid,
27   isActorPrivateKeyValid,
28   isActorPublicKeyValid
29 } from '../../helpers/custom-validators/activitypub/actor'
30 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
31 import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32 import { AccountModel } from '../account/account'
33 import { AvatarModel } from '../avatar/avatar'
34 import { ServerModel } from '../server/server'
35 import { isOutdated, throwIfNotValid } from '../utils'
36 import { VideoChannelModel } from '../video/video-channel'
37 import { ActorFollowModel } from './actor-follow'
38 import { VideoModel } from '../video/video'
39 import {
40   MActor,
41   MActorAccountChannelId,
42   MActorFormattable,
43   MActorFull, MActorHost,
44   MActorServer,
45   MActorSummaryFormattable,
46   MServerHost,
47   MActorRedundancyAllowed
48 } from '../../typings/models'
49 import * as Bluebird from 'bluebird'
50
51 enum ScopeNames {
52   FULL = 'FULL'
53 }
54
55 export const unusedActorAttributesForAPI = [
56   'publicKey',
57   'privateKey',
58   'inboxUrl',
59   'outboxUrl',
60   'sharedInboxUrl',
61   'followersUrl',
62   'followingUrl',
63   'url',
64   'createdAt',
65   'updatedAt'
66 ]
67
68 @DefaultScope(() => ({
69   include: [
70     {
71       model: ServerModel,
72       required: false
73     },
74     {
75       model: AvatarModel,
76       required: false
77     }
78   ]
79 }))
80 @Scopes(() => ({
81   [ScopeNames.FULL]: {
82     include: [
83       {
84         model: AccountModel.unscoped(),
85         required: false
86       },
87       {
88         model: VideoChannelModel.unscoped(),
89         required: false,
90         include: [
91           {
92             model: AccountModel,
93             required: true
94           }
95         ]
96       },
97       {
98         model: ServerModel,
99         required: false
100       },
101       {
102         model: AvatarModel,
103         required: false
104       }
105     ]
106   }
107 }))
108 @Table({
109   tableName: 'actor',
110   indexes: [
111     {
112       fields: [ 'url' ],
113       unique: true
114     },
115     {
116       fields: [ 'preferredUsername', 'serverId' ],
117       unique: true
118     },
119     {
120       fields: [ 'inboxUrl', 'sharedInboxUrl' ]
121     },
122     {
123       fields: [ 'sharedInboxUrl' ]
124     },
125     {
126       fields: [ 'serverId' ]
127     },
128     {
129       fields: [ 'avatarId' ]
130     },
131     {
132       fields: [ 'followersUrl' ]
133     }
134   ]
135 })
136 export class ActorModel extends Model<ActorModel> {
137
138   @AllowNull(false)
139   @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
140   type: ActivityPubActorType
141
142   @AllowNull(false)
143   @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
144   @Column
145   preferredUsername: string
146
147   @AllowNull(false)
148   @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
149   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
150   url: string
151
152   @AllowNull(true)
153   @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
154   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
155   publicKey: string
156
157   @AllowNull(true)
158   @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
159   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
160   privateKey: string
161
162   @AllowNull(false)
163   @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
164   @Column
165   followersCount: number
166
167   @AllowNull(false)
168   @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
169   @Column
170   followingCount: number
171
172   @AllowNull(false)
173   @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
174   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
175   inboxUrl: string
176
177   @AllowNull(false)
178   @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
179   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
180   outboxUrl: string
181
182   @AllowNull(false)
183   @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
184   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
185   sharedInboxUrl: string
186
187   @AllowNull(false)
188   @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
189   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
190   followersUrl: string
191
192   @AllowNull(false)
193   @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
194   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
195   followingUrl: string
196
197   @CreatedAt
198   createdAt: Date
199
200   @UpdatedAt
201   updatedAt: Date
202
203   @ForeignKey(() => AvatarModel)
204   @Column
205   avatarId: number
206
207   @BelongsTo(() => AvatarModel, {
208     foreignKey: {
209       allowNull: true
210     },
211     onDelete: 'set null',
212     hooks: true
213   })
214   Avatar: AvatarModel
215
216   @HasMany(() => ActorFollowModel, {
217     foreignKey: {
218       name: 'actorId',
219       allowNull: false
220     },
221     as: 'ActorFollowings',
222     onDelete: 'cascade'
223   })
224   ActorFollowing: ActorFollowModel[]
225
226   @HasMany(() => ActorFollowModel, {
227     foreignKey: {
228       name: 'targetActorId',
229       allowNull: false
230     },
231     as: 'ActorFollowers',
232     onDelete: 'cascade'
233   })
234   ActorFollowers: ActorFollowModel[]
235
236   @ForeignKey(() => ServerModel)
237   @Column
238   serverId: number
239
240   @BelongsTo(() => ServerModel, {
241     foreignKey: {
242       allowNull: true
243     },
244     onDelete: 'cascade'
245   })
246   Server: ServerModel
247
248   @HasOne(() => AccountModel, {
249     foreignKey: {
250       allowNull: true
251     },
252     onDelete: 'cascade',
253     hooks: true
254   })
255   Account: AccountModel
256
257   @HasOne(() => VideoChannelModel, {
258     foreignKey: {
259       allowNull: true
260     },
261     onDelete: 'cascade',
262     hooks: true
263   })
264   VideoChannel: VideoChannelModel
265
266   static load (id: number): Bluebird<MActor> {
267     return ActorModel.unscoped().findByPk(id)
268   }
269
270   static loadFull (id: number): Bluebird<MActorFull> {
271     return ActorModel.scope(ScopeNames.FULL).findByPk(id)
272   }
273
274   static loadFromAccountByVideoId (videoId: number, transaction: Sequelize.Transaction): Bluebird<MActor> {
275     const query = {
276       include: [
277         {
278           attributes: [ 'id' ],
279           model: AccountModel.unscoped(),
280           required: true,
281           include: [
282             {
283               attributes: [ 'id' ],
284               model: VideoChannelModel.unscoped(),
285               required: true,
286               include: [
287                 {
288                   attributes: [ 'id' ],
289                   model: VideoModel.unscoped(),
290                   required: true,
291                   where: {
292                     id: videoId
293                   }
294                 }
295               ]
296             }
297           ]
298         }
299       ],
300       transaction
301     }
302
303     return ActorModel.unscoped().findOne(query)
304   }
305
306   static isActorUrlExist (url: string) {
307     const query = {
308       raw: true,
309       where: {
310         url
311       }
312     }
313
314     return ActorModel.unscoped().findOne(query)
315       .then(a => !!a)
316   }
317
318   static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
319     const query = {
320       where: {
321         followersUrl: {
322           [ Sequelize.Op.in ]: followersUrls
323         }
324       },
325       transaction
326     }
327
328     return ActorModel.scope(ScopeNames.FULL).findAll(query)
329   }
330
331   static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
332     const query = {
333       where: {
334         preferredUsername,
335         serverId: null
336       },
337       transaction
338     }
339
340     return ActorModel.scope(ScopeNames.FULL).findOne(query)
341   }
342
343   static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
344     const query = {
345       where: {
346         preferredUsername
347       },
348       include: [
349         {
350           model: ServerModel,
351           required: true,
352           where: {
353             host
354           }
355         }
356       ]
357     }
358
359     return ActorModel.scope(ScopeNames.FULL).findOne(query)
360   }
361
362   static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
363     const query = {
364       where: {
365         url
366       },
367       transaction,
368       include: [
369         {
370           attributes: [ 'id' ],
371           model: AccountModel.unscoped(),
372           required: false
373         },
374         {
375           attributes: [ 'id' ],
376           model: VideoChannelModel.unscoped(),
377           required: false
378         }
379       ]
380     }
381
382     return ActorModel.unscoped().findOne(query)
383   }
384
385   static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
386     const query = {
387       where: {
388         url
389       },
390       transaction
391     }
392
393     return ActorModel.scope(ScopeNames.FULL).findOne(query)
394   }
395
396   static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
397     return ActorModel.increment(column, {
398       by,
399       where: {
400         id
401       }
402     })
403   }
404
405   toFormattedSummaryJSON (this: MActorSummaryFormattable) {
406     let avatar: Avatar = null
407     if (this.Avatar) {
408       avatar = this.Avatar.toFormattedJSON()
409     }
410
411     return {
412       url: this.url,
413       name: this.preferredUsername,
414       host: this.getHost(),
415       avatar
416     }
417   }
418
419   toFormattedJSON (this: MActorFormattable) {
420     const base = this.toFormattedSummaryJSON()
421
422     return Object.assign(base, {
423       id: this.id,
424       hostRedundancyAllowed: this.getRedundancyAllowed(),
425       followingCount: this.followingCount,
426       followersCount: this.followersCount,
427       createdAt: this.createdAt,
428       updatedAt: this.updatedAt
429     })
430   }
431
432   toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
433     let activityPubType
434     if (type === 'Account') {
435       activityPubType = 'Person' as 'Person'
436     } else if (type === 'Application') {
437       activityPubType = 'Application' as 'Application'
438     } else { // VideoChannel
439       activityPubType = 'Group' as 'Group'
440     }
441
442     let icon = undefined
443     if (this.avatarId) {
444       const extension = extname(this.Avatar.filename)
445       icon = {
446         type: 'Image',
447         mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
448         url: this.getAvatarUrl()
449       }
450     }
451
452     const json = {
453       type: activityPubType,
454       id: this.url,
455       following: this.getFollowingUrl(),
456       followers: this.getFollowersUrl(),
457       playlists: this.getPlaylistsUrl(),
458       inbox: this.inboxUrl,
459       outbox: this.outboxUrl,
460       preferredUsername: this.preferredUsername,
461       url: this.url,
462       name,
463       endpoints: {
464         sharedInbox: this.sharedInboxUrl
465       },
466       publicKey: {
467         id: this.getPublicKeyUrl(),
468         owner: this.url,
469         publicKeyPem: this.publicKey
470       },
471       icon
472     }
473
474     return activityPubContextify(json)
475   }
476
477   getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
478     const query = {
479       attributes: [ 'sharedInboxUrl' ],
480       include: [
481         {
482           attribute: [],
483           model: ActorFollowModel.unscoped(),
484           required: true,
485           as: 'ActorFollowing',
486           where: {
487             state: 'accepted',
488             targetActorId: this.id
489           }
490         }
491       ],
492       transaction: t
493     }
494
495     return ActorModel.findAll(query)
496       .then(accounts => accounts.map(a => a.sharedInboxUrl))
497   }
498
499   getFollowingUrl () {
500     return this.url + '/following'
501   }
502
503   getFollowersUrl () {
504     return this.url + '/followers'
505   }
506
507   getPlaylistsUrl () {
508     return this.url + '/playlists'
509   }
510
511   getPublicKeyUrl () {
512     return this.url + '#main-key'
513   }
514
515   isOwned () {
516     return this.serverId === null
517   }
518
519   getWebfingerUrl (this: MActorServer) {
520     return 'acct:' + this.preferredUsername + '@' + this.getHost()
521   }
522
523   getIdentifier () {
524     return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
525   }
526
527   getHost (this: MActorHost) {
528     return this.Server ? this.Server.host : WEBSERVER.HOST
529   }
530
531   getRedundancyAllowed (this: MActorRedundancyAllowed) {
532     return this.Server ? this.Server.redundancyAllowed : false
533   }
534
535   getAvatarUrl () {
536     if (!this.avatarId) return undefined
537
538     return WEBSERVER.URL + this.Avatar.getStaticPath()
539   }
540
541   isOutdated () {
542     if (this.isOwned()) return false
543
544     return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
545   }
546 }