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