Use RsaSignature2017
[oweals/peertube.git] / server / models / activitypub / actor.ts
1 import { values } from 'lodash'
2 import { join } from 'path'
3 import * as Sequelize from 'sequelize'
4 import {
5   AllowNull,
6   BelongsTo,
7   Column,
8   CreatedAt,
9   DataType,
10   Default, DefaultScope,
11   ForeignKey,
12   HasMany,
13   HasOne,
14   Is,
15   IsUUID,
16   Model,
17   Scopes,
18   Table,
19   UpdatedAt
20 } from 'sequelize-typescript'
21 import { ActivityPubActorType } from '../../../shared/models/activitypub'
22 import { Avatar } from '../../../shared/models/avatars/avatar.model'
23 import { activityPubContextify } from '../../helpers'
24 import {
25   isActivityPubUrlValid,
26   isActorFollowersCountValid,
27   isActorFollowingCountValid,
28   isActorNameValid,
29   isActorPrivateKeyValid,
30   isActorPublicKeyValid
31 } from '../../helpers/custom-validators/activitypub'
32 import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
33 import { AccountModel } from '../account/account'
34 import { AvatarModel } from '../avatar/avatar'
35 import { ServerModel } from '../server/server'
36 import { throwIfNotValid } from '../utils'
37 import { VideoChannelModel } from '../video/video-channel'
38 import { ActorFollowModel } from './actor-follow'
39
40 enum ScopeNames {
41   FULL = 'FULL'
42 }
43
44 @DefaultScope({
45   include: [
46     {
47       model: () => ServerModel,
48       required: false
49     }
50   ]
51 })
52 @Scopes({
53   [ScopeNames.FULL]: {
54     include: [
55       {
56         model: () => AccountModel,
57         required: false
58       },
59       {
60         model: () => VideoChannelModel,
61         required: false
62       },
63       {
64         model: () => ServerModel,
65         required: false
66       }
67     ]
68   }
69 })
70 @Table({
71   tableName: 'actor',
72   indexes: [
73     {
74       fields: [ 'name', 'serverId' ],
75       unique: true
76     }
77   ]
78 })
79 export class ActorModel extends Model<ActorModel> {
80
81   @AllowNull(false)
82   @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
83   type: ActivityPubActorType
84
85   @AllowNull(false)
86   @Default(DataType.UUIDV4)
87   @IsUUID(4)
88   @Column(DataType.UUID)
89   uuid: string
90
91   @AllowNull(false)
92   @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name'))
93   @Column
94   name: string
95
96   @AllowNull(false)
97   @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
98   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
99   url: string
100
101   @AllowNull(true)
102   @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key'))
103   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY.max))
104   publicKey: string
105
106   @AllowNull(true)
107   @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key'))
108   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY.max))
109   privateKey: string
110
111   @AllowNull(false)
112   @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
113   @Column
114   followersCount: number
115
116   @AllowNull(false)
117   @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
118   @Column
119   followingCount: number
120
121   @AllowNull(false)
122   @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
123   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
124   inboxUrl: string
125
126   @AllowNull(false)
127   @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
128   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
129   outboxUrl: string
130
131   @AllowNull(false)
132   @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
133   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
134   sharedInboxUrl: string
135
136   @AllowNull(false)
137   @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
138   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
139   followersUrl: string
140
141   @AllowNull(false)
142   @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
143   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
144   followingUrl: string
145
146   @CreatedAt
147   createdAt: Date
148
149   @UpdatedAt
150   updatedAt: Date
151
152   @ForeignKey(() => AvatarModel)
153   @Column
154   avatarId: number
155
156   @BelongsTo(() => AvatarModel, {
157     foreignKey: {
158       allowNull: true
159     },
160     onDelete: 'cascade'
161   })
162   Avatar: AvatarModel
163
164   @HasMany(() => ActorFollowModel, {
165     foreignKey: {
166       name: 'actorId',
167       allowNull: false
168     },
169     onDelete: 'cascade'
170   })
171   AccountFollowing: ActorFollowModel[]
172
173   @HasMany(() => ActorFollowModel, {
174     foreignKey: {
175       name: 'targetActorId',
176       allowNull: false
177     },
178     as: 'followers',
179     onDelete: 'cascade'
180   })
181   AccountFollowers: ActorFollowModel[]
182
183   @ForeignKey(() => ServerModel)
184   @Column
185   serverId: number
186
187   @BelongsTo(() => ServerModel, {
188     foreignKey: {
189       allowNull: true
190     },
191     onDelete: 'cascade'
192   })
193   Server: ServerModel
194
195   @HasOne(() => AccountModel, {
196     foreignKey: {
197       allowNull: true
198     },
199     onDelete: 'cascade'
200   })
201   Account: AccountModel
202
203   @HasOne(() => VideoChannelModel, {
204     foreignKey: {
205       allowNull: true
206     },
207     onDelete: 'cascade'
208   })
209   VideoChannel: VideoChannelModel
210
211   static load (id: number) {
212     return ActorModel.scope(ScopeNames.FULL).findById(id)
213   }
214
215   static loadByUUID (uuid: string) {
216     const query = {
217       where: {
218         uuid
219       }
220     }
221
222     return ActorModel.scope(ScopeNames.FULL).findOne(query)
223   }
224
225   static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
226     const query = {
227       where: {
228         followersUrl: {
229           [ Sequelize.Op.in ]: followersUrls
230         }
231       },
232       transaction
233     }
234
235     return ActorModel.scope(ScopeNames.FULL).findAll(query)
236   }
237
238   static loadLocalByName (name: string) {
239     const query = {
240       where: {
241         name,
242         serverId: null
243       }
244     }
245
246     return ActorModel.scope(ScopeNames.FULL).findOne(query)
247   }
248
249   static loadByNameAndHost (name: string, host: string) {
250     const query = {
251       where: {
252         name
253       },
254       include: [
255         {
256           model: ServerModel,
257           required: true,
258           where: {
259             host
260           }
261         }
262       ]
263     }
264
265     return ActorModel.scope(ScopeNames.FULL).findOne(query)
266   }
267
268   static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
269     const query = {
270       where: {
271         url
272       },
273       transaction
274     }
275
276     return ActorModel.scope(ScopeNames.FULL).findOne(query)
277   }
278
279   toFormattedJSON () {
280     let avatar: Avatar = null
281     if (this.Avatar) {
282       avatar = {
283         path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
284         createdAt: this.Avatar.createdAt,
285         updatedAt: this.Avatar.updatedAt
286       }
287     }
288
289     let host = CONFIG.WEBSERVER.HOST
290     let score: number
291     if (this.Server) {
292       host = this.Server.host
293       score = this.Server.score
294     }
295
296     return {
297       id: this.id,
298       uuid: this.uuid,
299       host,
300       score,
301       followingCount: this.followingCount,
302       followersCount: this.followersCount,
303       avatar
304     }
305   }
306
307   toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') {
308     let activityPubType
309     if (type === 'Account') {
310       activityPubType = 'Person' as 'Person'
311     } else if (type === 'Application') {
312       activityPubType = 'Application' as 'Application'
313     } else { // VideoChannel
314       activityPubType = 'Group' as 'Group'
315     }
316
317     const json = {
318       type: activityPubType,
319       id: this.url,
320       following: this.getFollowingUrl(),
321       followers: this.getFollowersUrl(),
322       inbox: this.inboxUrl,
323       outbox: this.outboxUrl,
324       preferredUsername,
325       url: this.url,
326       name: this.name,
327       endpoints: {
328         sharedInbox: this.sharedInboxUrl
329       },
330       uuid: this.uuid,
331       publicKey: {
332         id: this.getPublicKeyUrl(),
333         owner: this.url,
334         publicKeyPem: this.publicKey
335       }
336     }
337
338     return activityPubContextify(json)
339   }
340
341   getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
342     const query = {
343       attributes: [ 'sharedInboxUrl' ],
344       include: [
345         {
346           model: ActorFollowModel,
347           required: true,
348           as: 'followers',
349           where: {
350             targetActorId: this.id
351           }
352         }
353       ],
354       transaction: t
355     }
356
357     return ActorModel.findAll(query)
358       .then(accounts => accounts.map(a => a.sharedInboxUrl))
359   }
360
361   getFollowingUrl () {
362     return this.url + '/following'
363   }
364
365   getFollowersUrl () {
366     return this.url + '/followers'
367   }
368
369   getPublicKeyUrl () {
370     return this.url + '#main-key'
371   }
372
373   isOwned () {
374     return this.serverId === null
375   }
376 }