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