]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/actor/actor.ts
add response content for createVideoChannel endpoint in openapi spec
[github/Chocobozzz/PeerTube.git] / server / models / actor / actor.ts
CommitLineData
50d6de9c 1import { values } from 'lodash'
47564bbe 2import { extname } from 'path'
b49f22d8 3import { literal, Op, Transaction } from 'sequelize'
fadf619a 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'
b49f22d8 20import { ModelCache } from '@server/models/model-cache'
a1587156 21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
f4796856 22import { ActorImage } from '../../../shared/models/actors/actor-image.model'
da854ddd 23import { activityPubContextify } from '../../helpers/activitypub'
fadf619a 24import {
2422c46b
C
25 isActorFollowersCountValid,
26 isActorFollowingCountValid,
27 isActorPreferredUsernameValid,
28 isActorPrivateKeyValid,
da854ddd
C
29 isActorPublicKeyValid
30} from '../../helpers/custom-validators/activitypub/actor'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
2cb03dc1
C
32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
1ca9f7c3
C
40import {
41 MActor,
42 MActorAccountChannelId,
2cb03dc1
C
43 MActorAPAccount,
44 MActorAPChannel,
1ca9f7c3 45 MActorFormattable,
b5fecbf4
C
46 MActorFull,
47 MActorHost,
1ca9f7c3 48 MActorServer,
b49f22d8
C
49 MActorSummaryFormattable,
50 MActorUrl,
47581df0 51 MActorWithInboxes
26d6bf65 52} from '../../types/models'
b49f22d8 53import { AccountModel } from '../account/account'
b49f22d8
C
54import { ServerModel } from '../server/server'
55import { isOutdated, throwIfNotValid } from '../utils'
56import { VideoModel } from '../video/video'
57import { VideoChannelModel } from '../video/video-channel'
58import { ActorFollowModel } from './actor-follow'
7d9ba5c0 59import { ActorImageModel } from './actor-image'
fadf619a 60
50d6de9c
C
61enum ScopeNames {
62 FULL = 'FULL'
63}
64
f37dc0dd
C
65export const unusedActorAttributesForAPI = [
66 'publicKey',
67 'privateKey',
68 'inboxUrl',
69 'outboxUrl',
70 'sharedInboxUrl',
71 'followersUrl',
a66c2e32 72 'followingUrl'
f37dc0dd
C
73]
74
3acc5084 75@DefaultScope(() => ({
ce33ee01
C
76 include: [
77 {
3acc5084 78 model: ServerModel,
ce33ee01 79 required: false
c5911fd3
C
80 },
81 {
f4796856
C
82 model: ActorImageModel,
83 as: 'Avatar',
c5911fd3 84 required: false
ce33ee01
C
85 }
86 ]
3acc5084
C
87}))
88@Scopes(() => ({
50d6de9c
C
89 [ScopeNames.FULL]: {
90 include: [
91 {
3acc5084 92 model: AccountModel.unscoped(),
50d6de9c
C
93 required: false
94 },
95 {
3acc5084 96 model: VideoChannelModel.unscoped(),
c48e82b5
C
97 required: false,
98 include: [
99 {
3acc5084 100 model: AccountModel,
c48e82b5
C
101 required: true
102 }
103 ]
ce33ee01
C
104 },
105 {
3acc5084 106 model: ServerModel,
ce33ee01 107 required: false
c5911fd3
C
108 },
109 {
f4796856
C
110 model: ActorImageModel,
111 as: 'Avatar',
c5911fd3 112 required: false
2cb03dc1
C
113 },
114 {
115 model: ActorImageModel,
116 as: 'Banner',
117 required: false
50d6de9c 118 }
3acc5084 119 ]
50d6de9c 120 }
3acc5084 121}))
fadf619a 122@Table({
50d6de9c
C
123 tableName: 'actor',
124 indexes: [
2ccaeeb3 125 {
8cd72bd3
C
126 fields: [ 'url' ],
127 unique: true
2ccaeeb3 128 },
50d6de9c 129 {
e12a0092 130 fields: [ 'preferredUsername', 'serverId' ],
77e08517
C
131 unique: true,
132 where: {
133 serverId: {
134 [Op.ne]: null
135 }
136 }
137 },
0f06c4de
C
138 {
139 fields: [ 'preferredUsername' ],
140 unique: true,
141 where: {
142 serverId: null
143 }
144 },
6502c3d4
C
145 {
146 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
57c36b27 147 },
a3d1026b
C
148 {
149 fields: [ 'sharedInboxUrl' ]
150 },
57c36b27
C
151 {
152 fields: [ 'serverId' ]
153 },
154 {
155 fields: [ 'avatarId' ]
8cd72bd3 156 },
8cd72bd3
C
157 {
158 fields: [ 'followersUrl' ]
50d6de9c
C
159 }
160 ]
fadf619a 161})
b49f22d8 162export class ActorModel extends Model {
fadf619a 163
50d6de9c 164 @AllowNull(false)
3acc5084 165 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
50d6de9c
C
166 type: ActivityPubActorType
167
fadf619a 168 @AllowNull(false)
e12a0092 169 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
fadf619a 170 @Column
e12a0092 171 preferredUsername: string
fadf619a
C
172
173 @AllowNull(false)
174 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
01de67b9 175 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
176 url: string
177
178 @AllowNull(true)
1735c825 179 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
01de67b9 180 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
fadf619a
C
181 publicKey: string
182
183 @AllowNull(true)
1735c825 184 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
01de67b9 185 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
fadf619a
C
186 privateKey: string
187
188 @AllowNull(false)
189 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
190 @Column
191 followersCount: number
192
193 @AllowNull(false)
194 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
195 @Column
196 followingCount: number
197
198 @AllowNull(false)
199 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
01de67b9 200 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
201 inboxUrl: string
202
0b5c385b
C
203 @AllowNull(true)
204 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
01de67b9 205 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
206 outboxUrl: string
207
47581df0
C
208 @AllowNull(true)
209 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
01de67b9 210 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
211 sharedInboxUrl: string
212
0b5c385b
C
213 @AllowNull(true)
214 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
01de67b9 215 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
216 followersUrl: string
217
0b5c385b
C
218 @AllowNull(true)
219 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
01de67b9 220 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
221 followingUrl: string
222
a66c2e32
C
223 @AllowNull(true)
224 @Column
225 remoteCreatedAt: Date
226
fadf619a
C
227 @CreatedAt
228 createdAt: Date
229
230 @UpdatedAt
231 updatedAt: Date
232
f4796856 233 @ForeignKey(() => ActorImageModel)
fadf619a
C
234 @Column
235 avatarId: number
236
f4796856
C
237 @ForeignKey(() => ActorImageModel)
238 @Column
239 bannerId: number
240
241 @BelongsTo(() => ActorImageModel, {
242 foreignKey: {
243 name: 'avatarId',
244 allowNull: true
245 },
246 as: 'Avatar',
247 onDelete: 'set null',
248 hooks: true
249 })
250 Avatar: ActorImageModel
251
252 @BelongsTo(() => ActorImageModel, {
fadf619a 253 foreignKey: {
f4796856 254 name: 'bannerId',
fadf619a
C
255 allowNull: true
256 },
f4796856 257 as: 'Banner',
f05a1c30
C
258 onDelete: 'set null',
259 hooks: true
fadf619a 260 })
f4796856 261 Banner: ActorImageModel
fadf619a 262
50d6de9c 263 @HasMany(() => ActorFollowModel, {
fadf619a 264 foreignKey: {
50d6de9c 265 name: 'actorId',
fadf619a
C
266 allowNull: false
267 },
cef534ed 268 as: 'ActorFollowings',
fadf619a
C
269 onDelete: 'cascade'
270 })
54e74059 271 ActorFollowing: ActorFollowModel[]
fadf619a 272
50d6de9c 273 @HasMany(() => ActorFollowModel, {
fadf619a 274 foreignKey: {
50d6de9c 275 name: 'targetActorId',
fadf619a
C
276 allowNull: false
277 },
54e74059 278 as: 'ActorFollowers',
fadf619a
C
279 onDelete: 'cascade'
280 })
54e74059 281 ActorFollowers: ActorFollowModel[]
fadf619a
C
282
283 @ForeignKey(() => ServerModel)
284 @Column
285 serverId: number
286
287 @BelongsTo(() => ServerModel, {
288 foreignKey: {
289 allowNull: true
290 },
291 onDelete: 'cascade'
292 })
293 Server: ServerModel
294
50d6de9c
C
295 @HasOne(() => AccountModel, {
296 foreignKey: {
c5a893d5
C
297 allowNull: true
298 },
299 onDelete: 'cascade',
300 hooks: true
50d6de9c
C
301 })
302 Account: AccountModel
303
304 @HasOne(() => VideoChannelModel, {
305 foreignKey: {
c5a893d5
C
306 allowNull: true
307 },
308 onDelete: 'cascade',
309 hooks: true
50d6de9c
C
310 })
311 VideoChannel: VideoChannelModel
312
b49f22d8 313 static load (id: number): Promise<MActor> {
9b39106d 314 return ActorModel.unscoped().findByPk(id)
50d6de9c
C
315 }
316
b49f22d8 317 static loadFull (id: number): Promise<MActorFull> {
453e83ea
C
318 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
319 }
320
b49f22d8 321 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
e5565833
C
322 const query = {
323 include: [
324 {
325 attributes: [ 'id' ],
326 model: AccountModel.unscoped(),
327 required: true,
328 include: [
329 {
330 attributes: [ 'id' ],
331 model: VideoChannelModel.unscoped(),
332 required: true,
3acc5084
C
333 include: [
334 {
335 attributes: [ 'id' ],
336 model: VideoModel.unscoped(),
337 required: true,
338 where: {
339 id: videoId
340 }
e5565833 341 }
3acc5084 342 ]
e5565833
C
343 }
344 ]
345 }
346 ],
347 transaction
348 }
349
3acc5084 350 return ActorModel.unscoped().findOne(query)
e5565833
C
351 }
352
d4defe07
C
353 static isActorUrlExist (url: string) {
354 const query = {
355 raw: true,
356 where: {
357 url
358 }
359 }
360
361 return ActorModel.unscoped().findOne(query)
362 .then(a => !!a)
363 }
364
b49f22d8 365 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
fadf619a
C
366 const query = {
367 where: {
368 followersUrl: {
a1587156 369 [Op.in]: followersUrls
fadf619a
C
370 }
371 },
372 transaction
373 }
374
50d6de9c
C
375 return ActorModel.scope(ScopeNames.FULL).findAll(query)
376 }
377
b49f22d8 378 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
0ffd6d32
C
379 const fun = () => {
380 const query = {
381 where: {
382 preferredUsername,
383 serverId: null
384 },
385 transaction
386 }
e4a686b4 387
0ffd6d32
C
388 return ActorModel.scope(ScopeNames.FULL)
389 .findOne(query)
50d6de9c
C
390 }
391
0ffd6d32
C
392 return ModelCache.Instance.doCache({
393 cacheType: 'local-actor-name',
394 key: preferredUsername,
395 // The server actor never change, so we can easily cache it
396 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
397 fun
398 })
0374b6b5
C
399 }
400
b49f22d8 401 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
0ffd6d32
C
402 const fun = () => {
403 const query = {
404 attributes: [ 'url' ],
405 where: {
406 preferredUsername,
407 serverId: null
408 },
409 transaction
410 }
0374b6b5 411
0ffd6d32
C
412 return ActorModel.unscoped()
413 .findOne(query)
0374b6b5
C
414 }
415
0ffd6d32
C
416 return ModelCache.Instance.doCache({
417 cacheType: 'local-actor-name',
418 key: preferredUsername,
419 // The server actor never change, so we can easily cache it
420 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
421 fun
422 })
50d6de9c
C
423 }
424
b49f22d8 425 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
50d6de9c
C
426 const query = {
427 where: {
e12a0092 428 preferredUsername
50d6de9c
C
429 },
430 include: [
431 {
432 model: ServerModel,
433 required: true,
434 where: {
435 host
436 }
437 }
438 ]
439 }
440
441 return ActorModel.scope(ScopeNames.FULL).findOne(query)
442 }
443
b49f22d8 444 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
e587e0ec
C
445 const query = {
446 where: {
447 url
448 },
449 transaction,
450 include: [
451 {
452 attributes: [ 'id' ],
453 model: AccountModel.unscoped(),
454 required: false
455 },
456 {
457 attributes: [ 'id' ],
458 model: VideoChannelModel.unscoped(),
459 required: false
460 }
461 ]
462 }
463
464 return ActorModel.unscoped().findOne(query)
465 }
466
b49f22d8 467 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
50d6de9c
C
468 const query = {
469 where: {
470 url
471 },
472 transaction
473 }
474
475 return ActorModel.scope(ScopeNames.FULL).findOne(query)
fadf619a
C
476 }
477
e6122097
C
478 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
479 const sanitizedOfId = parseInt(ofId + '', 10)
480 const where = { id: sanitizedOfId }
481
482 let columnToUpdate: string
483 let columnOfCount: string
484
485 if (type === 'followers') {
486 columnToUpdate = 'followersCount'
487 columnOfCount = 'targetActorId'
488 } else {
489 columnToUpdate = 'followingCount'
490 columnOfCount = 'actorId'
491 }
492
493 return ActorModel.update({
494 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
495 }, { where, transaction })
32b2b43c
C
496 }
497
b49f22d8 498 static loadAccountActorByVideoId (videoId: number): Promise<MActor> {
2c8776fc
C
499 const query = {
500 include: [
501 {
502 attributes: [ 'id' ],
503 model: AccountModel.unscoped(),
504 required: true,
505 include: [
506 {
507 attributes: [ 'id', 'accountId' ],
508 model: VideoChannelModel.unscoped(),
509 required: true,
510 include: [
511 {
512 attributes: [ 'id', 'channelId' ],
513 model: VideoModel.unscoped(),
514 where: {
515 id: videoId
516 }
517 }
518 ]
519 }
520 ]
521 }
522 ]
523 }
524
525 return ActorModel.unscoped().findOne(query)
526 }
527
47581df0
C
528 getSharedInbox (this: MActorWithInboxes) {
529 return this.sharedInboxUrl || this.inboxUrl
530 }
531
1ca9f7c3 532 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
f4796856 533 let avatar: ActorImage = null
fadf619a 534 if (this.Avatar) {
c5911fd3 535 avatar = this.Avatar.toFormattedJSON()
fadf619a
C
536 }
537
fadf619a 538 return {
4cb6d457 539 url: this.url,
60650c77 540 name: this.preferredUsername,
e12a0092 541 host: this.getHost(),
1ca9f7c3
C
542 avatar
543 }
544 }
545
546 toFormattedJSON (this: MActorFormattable) {
547 const base = this.toFormattedSummaryJSON()
548
2cb03dc1 549 let banner: ActorImage = null
91723454 550 if (this.Banner) {
2cb03dc1
C
551 banner = this.Banner.toFormattedJSON()
552 }
553
1ca9f7c3
C
554 return Object.assign(base, {
555 id: this.id,
c48e82b5 556 hostRedundancyAllowed: this.getRedundancyAllowed(),
fadf619a
C
557 followingCount: this.followingCount,
558 followersCount: this.followersCount,
2cb03dc1 559 banner,
e024fd6a 560 createdAt: this.getCreatedAt()
1ca9f7c3 561 })
fadf619a
C
562 }
563
2cb03dc1 564 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
a1587156 565 let icon: ActivityIconObject
2cb03dc1 566 let image: ActivityIconObject
a1587156 567
c5911fd3
C
568 if (this.avatarId) {
569 const extension = extname(this.Avatar.filename)
a1587156 570
c5911fd3
C
571 icon = {
572 type: 'Image',
2cb03dc1 573 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
84531547
C
574 height: this.Avatar.height,
575 width: this.Avatar.width,
c5911fd3
C
576 url: this.getAvatarUrl()
577 }
578 }
579
2cb03dc1 580 if (this.bannerId) {
84531547
C
581 const banner = (this as MActorAPChannel).Banner
582 const extension = extname(banner.filename)
2cb03dc1
C
583
584 image = {
585 type: 'Image',
586 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
84531547
C
587 height: banner.height,
588 width: banner.width,
2cb03dc1
C
589 url: this.getBannerUrl()
590 }
591 }
592
fadf619a 593 const json = {
8424c402 594 type: this.type,
fadf619a
C
595 id: this.url,
596 following: this.getFollowingUrl(),
597 followers: this.getFollowersUrl(),
418d092a 598 playlists: this.getPlaylistsUrl(),
fadf619a
C
599 inbox: this.inboxUrl,
600 outbox: this.outboxUrl,
e12a0092 601 preferredUsername: this.preferredUsername,
fadf619a 602 url: this.url,
e12a0092 603 name,
fadf619a
C
604 endpoints: {
605 sharedInbox: this.sharedInboxUrl
606 },
fadf619a
C
607 publicKey: {
608 id: this.getPublicKeyUrl(),
609 owner: this.url,
610 publicKeyPem: this.publicKey
c5911fd3 611 },
a66c2e32 612 published: this.getCreatedAt().toISOString(),
2cb03dc1
C
613 icon,
614 image
fadf619a
C
615 }
616
617 return activityPubContextify(json)
618 }
619
941c5eac 620 getFollowerSharedInboxUrls (t: Transaction) {
fadf619a
C
621 const query = {
622 attributes: [ 'sharedInboxUrl' ],
623 include: [
624 {
54e74059
C
625 attribute: [],
626 model: ActorFollowModel.unscoped(),
fadf619a 627 required: true,
d6e99e53 628 as: 'ActorFollowing',
fadf619a 629 where: {
54e74059 630 state: 'accepted',
50d6de9c 631 targetActorId: this.id
fadf619a
C
632 }
633 }
634 ],
635 transaction: t
636 }
637
638 return ActorModel.findAll(query)
639 .then(accounts => accounts.map(a => a.sharedInboxUrl))
640 }
641
642 getFollowingUrl () {
643 return this.url + '/following'
644 }
645
646 getFollowersUrl () {
647 return this.url + '/followers'
648 }
649
418d092a
C
650 getPlaylistsUrl () {
651 return this.url + '/playlists'
652 }
653
fadf619a
C
654 getPublicKeyUrl () {
655 return this.url + '#main-key'
656 }
657
658 isOwned () {
659 return this.serverId === null
660 }
e12a0092 661
1ca9f7c3 662 getWebfingerUrl (this: MActorServer) {
e12a0092
C
663 return 'acct:' + this.preferredUsername + '@' + this.getHost()
664 }
665
80e36cd9
AB
666 getIdentifier () {
667 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
668 }
669
1ca9f7c3 670 getHost (this: MActorHost) {
6dd9de95 671 return this.Server ? this.Server.host : WEBSERVER.HOST
e12a0092 672 }
c5911fd3 673
b5fecbf4 674 getRedundancyAllowed () {
c48e82b5
C
675 return this.Server ? this.Server.redundancyAllowed : false
676 }
677
c5911fd3
C
678 getAvatarUrl () {
679 if (!this.avatarId) return undefined
680
557b13ae 681 return WEBSERVER.URL + this.Avatar.getStaticPath()
c5911fd3 682 }
a5625b41 683
2cb03dc1
C
684 getBannerUrl () {
685 if (!this.bannerId) return undefined
686
687 return WEBSERVER.URL + this.Banner.getStaticPath()
688 }
689
a5625b41
C
690 isOutdated () {
691 if (this.isOwned()) return false
692
9f79ade6 693 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
a5625b41 694 }
a66c2e32
C
695
696 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
697 return this.remoteCreatedAt || this.createdAt
698 }
fadf619a 699}