Merge branch 'develop' into cli-wrapper
[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   Default,
11   DefaultScope,
12   ForeignKey,
13   HasMany,
14   HasOne,
15   Is,
16   IsUUID,
17   Model,
18   Scopes,
19   Table,
20   UpdatedAt
21 } from 'sequelize-typescript'
22 import { ActivityPubActorType } from '../../../shared/models/activitypub'
23 import { Avatar } from '../../../shared/models/avatars/avatar.model'
24 import { activityPubContextify } from '../../helpers/activitypub'
25 import {
26   isActorFollowersCountValid,
27   isActorFollowingCountValid,
28   isActorPreferredUsernameValid,
29   isActorPrivateKeyValid,
30   isActorPublicKeyValid
31 } from '../../helpers/custom-validators/activitypub/actor'
32 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
33 import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
34 import { AccountModel } from '../account/account'
35 import { AvatarModel } from '../avatar/avatar'
36 import { ServerModel } from '../server/server'
37 import { throwIfNotValid } from '../utils'
38 import { VideoChannelModel } from '../video/video-channel'
39 import { ActorFollowModel } from './actor-follow'
40
41 enum ScopeNames {
42   FULL = 'FULL'
43 }
44
45 export const unusedActorAttributesForAPI = [
46   'publicKey',
47   'privateKey',
48   'inboxUrl',
49   'outboxUrl',
50   'sharedInboxUrl',
51   'followersUrl',
52   'followingUrl',
53   'url',
54   'createdAt',
55   'updatedAt'
56 ]
57
58 @DefaultScope({
59   include: [
60     {
61       model: () => ServerModel,
62       required: false
63     },
64     {
65       model: () => AvatarModel,
66       required: false
67     }
68   ]
69 })
70 @Scopes({
71   [ScopeNames.FULL]: {
72     include: [
73       {
74         model: () => AccountModel.unscoped(),
75         required: false
76       },
77       {
78         model: () => VideoChannelModel.unscoped(),
79         required: false,
80         include: [
81           {
82             model: () => AccountModel,
83             required: true
84           }
85         ]
86       },
87       {
88         model: () => ServerModel,
89         required: false
90       },
91       {
92         model: () => AvatarModel,
93         required: false
94       }
95     ]
96   }
97 })
98 @Table({
99   tableName: 'actor',
100   indexes: [
101     {
102       fields: [ 'url' ],
103       unique: true
104     },
105     {
106       fields: [ 'preferredUsername', 'serverId' ],
107       unique: true
108     },
109     {
110       fields: [ 'inboxUrl', 'sharedInboxUrl' ]
111     },
112     {
113       fields: [ 'sharedInboxUrl' ]
114     },
115     {
116       fields: [ 'serverId' ]
117     },
118     {
119       fields: [ 'avatarId' ]
120     },
121     {
122       fields: [ 'uuid' ],
123       unique: true
124     },
125     {
126       fields: [ 'followersUrl' ]
127     }
128   ]
129 })
130 export class ActorModel extends Model<ActorModel> {
131
132   @AllowNull(false)
133   @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
134   type: ActivityPubActorType
135
136   @AllowNull(false)
137   @Default(DataType.UUIDV4)
138   @IsUUID(4)
139   @Column(DataType.UUID)
140   uuid: string
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'))
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'))
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     onDelete: 'cascade'
222   })
223   ActorFollowing: ActorFollowModel[]
224
225   @HasMany(() => ActorFollowModel, {
226     foreignKey: {
227       name: 'targetActorId',
228       allowNull: false
229     },
230     as: 'ActorFollowers',
231     onDelete: 'cascade'
232   })
233   ActorFollowers: ActorFollowModel[]
234
235   @ForeignKey(() => ServerModel)
236   @Column
237   serverId: number
238
239   @BelongsTo(() => ServerModel, {
240     foreignKey: {
241       allowNull: true
242     },
243     onDelete: 'cascade'
244   })
245   Server: ServerModel
246
247   @HasOne(() => AccountModel, {
248     foreignKey: {
249       allowNull: true
250     },
251     onDelete: 'cascade',
252     hooks: true
253   })
254   Account: AccountModel
255
256   @HasOne(() => VideoChannelModel, {
257     foreignKey: {
258       allowNull: true
259     },
260     onDelete: 'cascade',
261     hooks: true
262   })
263   VideoChannel: VideoChannelModel
264
265   static load (id: number) {
266     return ActorModel.unscoped().findById(id)
267   }
268
269   static isActorUrlExist (url: string) {
270     const query = {
271       raw: true,
272       where: {
273         url
274       }
275     }
276
277     return ActorModel.unscoped().findOne(query)
278       .then(a => !!a)
279   }
280
281   static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
282     const query = {
283       where: {
284         followersUrl: {
285           [ Sequelize.Op.in ]: followersUrls
286         }
287       },
288       transaction
289     }
290
291     return ActorModel.scope(ScopeNames.FULL).findAll(query)
292   }
293
294   static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) {
295     const query = {
296       where: {
297         preferredUsername,
298         serverId: null
299       },
300       transaction
301     }
302
303     return ActorModel.scope(ScopeNames.FULL).findOne(query)
304   }
305
306   static loadByNameAndHost (preferredUsername: string, host: string) {
307     const query = {
308       where: {
309         preferredUsername
310       },
311       include: [
312         {
313           model: ServerModel,
314           required: true,
315           where: {
316             host
317           }
318         }
319       ]
320     }
321
322     return ActorModel.scope(ScopeNames.FULL).findOne(query)
323   }
324
325   static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
326     const query = {
327       where: {
328         url
329       },
330       transaction,
331       include: [
332         {
333           attributes: [ 'id' ],
334           model: AccountModel.unscoped(),
335           required: false
336         },
337         {
338           attributes: [ 'id' ],
339           model: VideoChannelModel.unscoped(),
340           required: false
341         }
342       ]
343     }
344
345     return ActorModel.unscoped().findOne(query)
346   }
347
348   static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) {
349     const query = {
350       where: {
351         url
352       },
353       transaction
354     }
355
356     return ActorModel.scope(ScopeNames.FULL).findOne(query)
357   }
358
359   static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
360     // FIXME: typings
361     return (ActorModel as any).increment(column, {
362       by,
363       where: {
364         id
365       }
366     })
367   }
368
369   toFormattedJSON () {
370     let avatar: Avatar = null
371     if (this.Avatar) {
372       avatar = this.Avatar.toFormattedJSON()
373     }
374
375     return {
376       id: this.id,
377       url: this.url,
378       uuid: this.uuid,
379       name: this.preferredUsername,
380       host: this.getHost(),
381       hostRedundancyAllowed: this.getRedundancyAllowed(),
382       followingCount: this.followingCount,
383       followersCount: this.followersCount,
384       avatar,
385       createdAt: this.createdAt,
386       updatedAt: this.updatedAt
387     }
388   }
389
390   toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
391     let activityPubType
392     if (type === 'Account') {
393       activityPubType = 'Person' as 'Person'
394     } else if (type === 'Application') {
395       activityPubType = 'Application' as 'Application'
396     } else { // VideoChannel
397       activityPubType = 'Group' as 'Group'
398     }
399
400     let icon = undefined
401     if (this.avatarId) {
402       const extension = extname(this.Avatar.filename)
403       icon = {
404         type: 'Image',
405         mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
406         url: this.getAvatarUrl()
407       }
408     }
409
410     const json = {
411       type: activityPubType,
412       id: this.url,
413       following: this.getFollowingUrl(),
414       followers: this.getFollowersUrl(),
415       inbox: this.inboxUrl,
416       outbox: this.outboxUrl,
417       preferredUsername: this.preferredUsername,
418       url: this.url,
419       name,
420       endpoints: {
421         sharedInbox: this.sharedInboxUrl
422       },
423       uuid: this.uuid,
424       publicKey: {
425         id: this.getPublicKeyUrl(),
426         owner: this.url,
427         publicKeyPem: this.publicKey
428       },
429       icon
430     }
431
432     return activityPubContextify(json)
433   }
434
435   getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
436     const query = {
437       attributes: [ 'sharedInboxUrl' ],
438       include: [
439         {
440           attribute: [],
441           model: ActorFollowModel.unscoped(),
442           required: true,
443           as: 'ActorFollowing',
444           where: {
445             state: 'accepted',
446             targetActorId: this.id
447           }
448         }
449       ],
450       transaction: t
451     }
452
453     return ActorModel.findAll(query)
454       .then(accounts => accounts.map(a => a.sharedInboxUrl))
455   }
456
457   getFollowingUrl () {
458     return this.url + '/following'
459   }
460
461   getFollowersUrl () {
462     return this.url + '/followers'
463   }
464
465   getPublicKeyUrl () {
466     return this.url + '#main-key'
467   }
468
469   isOwned () {
470     return this.serverId === null
471   }
472
473   getWebfingerUrl () {
474     return 'acct:' + this.preferredUsername + '@' + this.getHost()
475   }
476
477   getIdentifier () {
478     return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
479   }
480
481   getHost () {
482     return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
483   }
484
485   getRedundancyAllowed () {
486     return this.Server ? this.Server.redundancyAllowed : false
487   }
488
489   getAvatarUrl () {
490     if (!this.avatarId) return undefined
491
492     return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
493   }
494
495   isOutdated () {
496     if (this.isOwned()) return false
497
498     const now = Date.now()
499     const createdAtTime = this.createdAt.getTime()
500     const updatedAtTime = this.updatedAt.getTime()
501
502     return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
503       (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
504   }
505 }