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