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