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