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