diff options
author | kontrollanten <6680299+kontrollanten@users.noreply.github.com> | 2022-02-28 08:34:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-28 08:34:43 +0100 |
commit | d0800f7661f13fabe7bb6f4aa0ea50764f106405 (patch) | |
tree | d43e6b0b6f4a5a32e03487e6464edbcaf288be2a /server/models/actor | |
parent | 5cad2ca9db9b9d138f8a33058d10b94a9fd50c69 (diff) | |
download | PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.gz PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.zst PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.zip |
Implement avatar miniatures (#4639)
* client: remove unused file
* refactor(client/my-actor-avatar): size from input
Read size from component input instead of scss, to make it possible to
use smaller avatar images when implemented.
* implement avatar miniatures
close #4560
* fix(test): max file size
* fix(search-index): normalize res acc to avatarMini
* refactor avatars to an array
* client/search: resize channel avatar to 120
* refactor(client/videos): remove unused function
* client(actor-avatar): set default size
* fix tests and avatars full result
When findOne is used only an array containting one avatar is returned.
* update migration version and version notations
* server/search: harmonize normalizing
* Cleanup avatar miniature PR
Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server/models/actor')
-rw-r--r-- | server/models/actor/actor-follow.ts | 263 | ||||
-rw-r--r-- | server/models/actor/actor-image.ts | 67 | ||||
-rw-r--r-- | server/models/actor/actor.ts | 129 |
3 files changed, 259 insertions, 200 deletions
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 006282530..0f4d3c0a6 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { difference, values } from 'lodash' | 1 | import { difference, values } from 'lodash' |
2 | import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' | 2 | import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AfterCreate, | 4 | AfterCreate, |
5 | AfterDestroy, | 5 | AfterDestroy, |
@@ -30,12 +30,12 @@ import { | |||
30 | MActorFollowFormattable, | 30 | MActorFollowFormattable, |
31 | MActorFollowSubscriptions | 31 | MActorFollowSubscriptions |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { AttributesOnly } from '@shared/typescript-utils' | ||
34 | import { ActivityPubActorType } from '@shared/models' | 33 | import { ActivityPubActorType } from '@shared/models' |
34 | import { AttributesOnly } from '@shared/typescript-utils' | ||
35 | import { FollowState } from '../../../shared/models/actors' | 35 | import { FollowState } from '../../../shared/models/actors' |
36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
37 | import { logger } from '../../helpers/logger' | 37 | import { logger } from '../../helpers/logger' |
38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants' |
39 | import { AccountModel } from '../account/account' | 39 | import { AccountModel } from '../account/account' |
40 | import { ServerModel } from '../server/server' | 40 | import { ServerModel } from '../server/server' |
41 | import { doesExist } from '../shared/query' | 41 | import { doesExist } from '../shared/query' |
@@ -375,43 +375,46 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
375 | Object.assign(followingWhere, { type: actorType }) | 375 | Object.assign(followingWhere, { type: actorType }) |
376 | } | 376 | } |
377 | 377 | ||
378 | const query = { | 378 | const getQuery = (forCount: boolean) => { |
379 | distinct: true, | 379 | const actorModel = forCount |
380 | offset: start, | 380 | ? ActorModel.unscoped() |
381 | limit: count, | 381 | : ActorModel |
382 | order: getFollowsSort(sort), | 382 | |
383 | where: followWhere, | 383 | return { |
384 | include: [ | 384 | distinct: true, |
385 | { | 385 | offset: start, |
386 | model: ActorModel, | 386 | limit: count, |
387 | required: true, | 387 | order: getFollowsSort(sort), |
388 | as: 'ActorFollower', | 388 | where: followWhere, |
389 | where: { | 389 | include: [ |
390 | id | 390 | { |
391 | } | 391 | model: actorModel, |
392 | }, | 392 | required: true, |
393 | { | 393 | as: 'ActorFollower', |
394 | model: ActorModel, | 394 | where: { |
395 | as: 'ActorFollowing', | 395 | id |
396 | required: true, | ||
397 | where: followingWhere, | ||
398 | include: [ | ||
399 | { | ||
400 | model: ServerModel, | ||
401 | required: true | ||
402 | } | 396 | } |
403 | ] | 397 | }, |
404 | } | 398 | { |
405 | ] | 399 | model: actorModel, |
400 | as: 'ActorFollowing', | ||
401 | required: true, | ||
402 | where: followingWhere, | ||
403 | include: [ | ||
404 | { | ||
405 | model: ServerModel, | ||
406 | required: true | ||
407 | } | ||
408 | ] | ||
409 | } | ||
410 | ] | ||
411 | } | ||
406 | } | 412 | } |
407 | 413 | ||
408 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 414 | return Promise.all([ |
409 | .then(({ rows, count }) => { | 415 | ActorFollowModel.count(getQuery(true)), |
410 | return { | 416 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
411 | data: rows, | 417 | ]).then(([ total, data ]) => ({ total, data })) |
412 | total: count | ||
413 | } | ||
414 | }) | ||
415 | } | 418 | } |
416 | 419 | ||
417 | static listFollowersForApi (options: { | 420 | static listFollowersForApi (options: { |
@@ -429,11 +432,17 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
429 | const followerWhere: WhereOptions = {} | 432 | const followerWhere: WhereOptions = {} |
430 | 433 | ||
431 | if (search) { | 434 | if (search) { |
432 | Object.assign(followWhere, { | 435 | const escapedSearch = ActorFollowModel.sequelize.escape('%' + search + '%') |
433 | [Op.or]: [ | 436 | |
434 | searchAttribute(search, '$ActorFollower.preferredUsername$'), | 437 | Object.assign(followerWhere, { |
435 | searchAttribute(search, '$ActorFollower.Server.host$') | 438 | id: { |
436 | ] | 439 | [Op.in]: literal( |
440 | `(` + | ||
441 | `SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" ` + | ||
442 | `WHERE "preferredUsername" ILIKE ${escapedSearch} OR "host" ILIKE ${escapedSearch}` + | ||
443 | `)` | ||
444 | ) | ||
445 | } | ||
437 | }) | 446 | }) |
438 | } | 447 | } |
439 | 448 | ||
@@ -441,39 +450,43 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
441 | Object.assign(followerWhere, { type: actorType }) | 450 | Object.assign(followerWhere, { type: actorType }) |
442 | } | 451 | } |
443 | 452 | ||
444 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
445 | distinct: true, | 454 | const actorModel = forCount |
446 | offset: start, | 455 | ? ActorModel.unscoped() |
447 | limit: count, | 456 | : ActorModel |
448 | order: getFollowsSort(sort), | 457 | |
449 | where: followWhere, | 458 | return { |
450 | include: [ | 459 | distinct: true, |
451 | { | 460 | |
452 | model: ActorModel, | 461 | offset: start, |
453 | required: true, | 462 | limit: count, |
454 | as: 'ActorFollower', | 463 | order: getFollowsSort(sort), |
455 | where: followerWhere | 464 | where: followWhere, |
456 | }, | 465 | include: [ |
457 | { | 466 | { |
458 | model: ActorModel, | 467 | model: actorModel, |
459 | as: 'ActorFollowing', | 468 | required: true, |
460 | required: true, | 469 | as: 'ActorFollower', |
461 | where: { | 470 | where: followerWhere |
462 | id: { | 471 | }, |
463 | [Op.in]: actorIds | 472 | { |
473 | model: actorModel, | ||
474 | as: 'ActorFollowing', | ||
475 | required: true, | ||
476 | where: { | ||
477 | id: { | ||
478 | [Op.in]: actorIds | ||
479 | } | ||
464 | } | 480 | } |
465 | } | 481 | } |
466 | } | 482 | ] |
467 | ] | 483 | } |
468 | } | 484 | } |
469 | 485 | ||
470 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 486 | return Promise.all([ |
471 | .then(({ rows, count }) => { | 487 | ActorFollowModel.count(getQuery(true)), |
472 | return { | 488 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
473 | data: rows, | 489 | ]).then(([ total, data ]) => ({ total, data })) |
474 | total: count | ||
475 | } | ||
476 | }) | ||
477 | } | 490 | } |
478 | 491 | ||
479 | static listSubscriptionsForApi (options: { | 492 | static listSubscriptionsForApi (options: { |
@@ -497,58 +510,68 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
497 | }) | 510 | }) |
498 | } | 511 | } |
499 | 512 | ||
500 | const query = { | 513 | const getQuery = (forCount: boolean) => { |
501 | attributes: [], | 514 | let channelInclude: Includeable[] = [] |
502 | distinct: true, | 515 | |
503 | offset: start, | 516 | if (forCount !== true) { |
504 | limit: count, | 517 | channelInclude = [ |
505 | order: getSort(sort), | 518 | { |
506 | where, | 519 | attributes: { |
507 | include: [ | 520 | exclude: unusedActorAttributesForAPI |
508 | { | 521 | }, |
509 | attributes: [ 'id' ], | 522 | model: ActorModel, |
510 | model: ActorModel.unscoped(), | 523 | required: true |
511 | as: 'ActorFollowing', | 524 | }, |
512 | required: true, | 525 | { |
513 | include: [ | 526 | model: AccountModel.unscoped(), |
514 | { | 527 | required: true, |
515 | model: VideoChannelModel.unscoped(), | 528 | include: [ |
516 | required: true, | 529 | { |
517 | include: [ | 530 | attributes: { |
518 | { | 531 | exclude: unusedActorAttributesForAPI |
519 | attributes: { | ||
520 | exclude: unusedActorAttributesForAPI | ||
521 | }, | ||
522 | model: ActorModel, | ||
523 | required: true | ||
524 | }, | 532 | }, |
525 | { | 533 | model: ActorModel, |
526 | model: AccountModel.unscoped(), | 534 | required: true |
527 | required: true, | 535 | } |
528 | include: [ | 536 | ] |
529 | { | 537 | } |
530 | attributes: { | 538 | ] |
531 | exclude: unusedActorAttributesForAPI | 539 | } |
532 | }, | 540 | |
533 | model: ActorModel, | 541 | return { |
534 | required: true | 542 | attributes: forCount === true |
535 | } | 543 | ? [] |
536 | ] | 544 | : SORTABLE_COLUMNS.USER_SUBSCRIPTIONS, |
537 | } | 545 | distinct: true, |
538 | ] | 546 | offset: start, |
539 | } | 547 | limit: count, |
540 | ] | 548 | order: getSort(sort), |
541 | } | 549 | where, |
542 | ] | 550 | include: [ |
551 | { | ||
552 | attributes: [ 'id' ], | ||
553 | model: ActorModel.unscoped(), | ||
554 | as: 'ActorFollowing', | ||
555 | required: true, | ||
556 | include: [ | ||
557 | { | ||
558 | model: VideoChannelModel.unscoped(), | ||
559 | required: true, | ||
560 | include: channelInclude | ||
561 | } | ||
562 | ] | ||
563 | } | ||
564 | ] | ||
565 | } | ||
543 | } | 566 | } |
544 | 567 | ||
545 | return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query) | 568 | return Promise.all([ |
546 | .then(({ rows, count }) => { | 569 | ActorFollowModel.count(getQuery(true)), |
547 | return { | 570 | ActorFollowModel.findAll<MActorFollowSubscriptions>(getQuery(false)) |
548 | data: rows.map(r => r.ActorFollowing.VideoChannel), | 571 | ]).then(([ total, rows ]) => ({ |
549 | total: count | 572 | total, |
550 | } | 573 | data: rows.map(r => r.ActorFollowing.VideoChannel) |
551 | }) | 574 | })) |
552 | } | 575 | } |
553 | 576 | ||
554 | static async keepUnfollowedInstance (hosts: string[]) { | 577 | static async keepUnfollowedInstance (hosts: string[]) { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 8edff5ab4..f74ab735e 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -1,15 +1,29 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { |
4 | import { MActorImageFormattable } from '@server/types/models' | 4 | AfterDestroy, |
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { MActorImage, MActorImageFormattable } from '@server/types/models' | ||
17 | import { getLowercaseExtension } from '@shared/core-utils' | ||
18 | import { ActivityIconObject, ActorImageType } from '@shared/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 20 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
9 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
11 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
12 | import { throwIfNotValid } from '../utils' | 25 | import { throwIfNotValid } from '../utils' |
26 | import { ActorModel } from './actor' | ||
13 | 27 | ||
14 | @Table({ | 28 | @Table({ |
15 | tableName: 'actorImage', | 29 | tableName: 'actorImage', |
@@ -17,6 +31,10 @@ import { throwIfNotValid } from '../utils' | |||
17 | { | 31 | { |
18 | fields: [ 'filename' ], | 32 | fields: [ 'filename' ], |
19 | unique: true | 33 | unique: true |
34 | }, | ||
35 | { | ||
36 | fields: [ 'actorId', 'type', 'width' ], | ||
37 | unique: true | ||
20 | } | 38 | } |
21 | ] | 39 | ] |
22 | }) | 40 | }) |
@@ -55,6 +73,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
55 | @UpdatedAt | 73 | @UpdatedAt |
56 | updatedAt: Date | 74 | updatedAt: Date |
57 | 75 | ||
76 | @ForeignKey(() => ActorModel) | ||
77 | @Column | ||
78 | actorId: number | ||
79 | |||
80 | @BelongsTo(() => ActorModel, { | ||
81 | foreignKey: { | ||
82 | allowNull: false | ||
83 | }, | ||
84 | onDelete: 'CASCADE' | ||
85 | }) | ||
86 | Actor: ActorModel | ||
87 | |||
58 | @AfterDestroy | 88 | @AfterDestroy |
59 | static removeFilesAndSendDelete (instance: ActorImageModel) { | 89 | static removeFilesAndSendDelete (instance: ActorImageModel) { |
60 | logger.info('Removing actor image file %s.', instance.filename) | 90 | logger.info('Removing actor image file %s.', instance.filename) |
@@ -74,20 +104,41 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
74 | return ActorImageModel.findOne(query) | 104 | return ActorImageModel.findOne(query) |
75 | } | 105 | } |
76 | 106 | ||
107 | static getImageUrl (image: MActorImage) { | ||
108 | if (!image) return undefined | ||
109 | |||
110 | return WEBSERVER.URL + image.getStaticPath() | ||
111 | } | ||
112 | |||
77 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | 113 | toFormattedJSON (this: MActorImageFormattable): ActorImage { |
78 | return { | 114 | return { |
115 | width: this.width, | ||
79 | path: this.getStaticPath(), | 116 | path: this.getStaticPath(), |
80 | createdAt: this.createdAt, | 117 | createdAt: this.createdAt, |
81 | updatedAt: this.updatedAt | 118 | updatedAt: this.updatedAt |
82 | } | 119 | } |
83 | } | 120 | } |
84 | 121 | ||
85 | getStaticPath () { | 122 | toActivityPubObject (): ActivityIconObject { |
86 | if (this.type === ActorImageType.AVATAR) { | 123 | const extension = getLowercaseExtension(this.filename) |
87 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | 124 | |
125 | return { | ||
126 | type: 'Image', | ||
127 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
128 | height: this.height, | ||
129 | width: this.width, | ||
130 | url: ActorImageModel.getImageUrl(this) | ||
88 | } | 131 | } |
132 | } | ||
89 | 133 | ||
90 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | 134 | getStaticPath () { |
135 | switch (this.type) { | ||
136 | case ActorImageType.AVATAR: | ||
137 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
138 | |||
139 | case ActorImageType.BANNER: | ||
140 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
141 | } | ||
91 | } | 142 | } |
92 | 143 | ||
93 | getPath () { | 144 | getPath () { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index c12dcf634..08cb2fd24 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -16,11 +16,11 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
19 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
20 | import { getLowercaseExtension } from '@shared/core-utils' | 21 | import { getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | ||
23 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
24 | import { activityPubContextify } from '../../helpers/activitypub' | 24 | import { activityPubContextify } from '../../helpers/activitypub' |
25 | import { | 25 | import { |
26 | isActorFollowersCountValid, | 26 | isActorFollowersCountValid, |
@@ -81,7 +81,7 @@ export const unusedActorAttributesForAPI = [ | |||
81 | }, | 81 | }, |
82 | { | 82 | { |
83 | model: ActorImageModel, | 83 | model: ActorImageModel, |
84 | as: 'Avatar', | 84 | as: 'Avatars', |
85 | required: false | 85 | required: false |
86 | } | 86 | } |
87 | ] | 87 | ] |
@@ -109,12 +109,12 @@ export const unusedActorAttributesForAPI = [ | |||
109 | }, | 109 | }, |
110 | { | 110 | { |
111 | model: ActorImageModel, | 111 | model: ActorImageModel, |
112 | as: 'Avatar', | 112 | as: 'Avatars', |
113 | required: false | 113 | required: false |
114 | }, | 114 | }, |
115 | { | 115 | { |
116 | model: ActorImageModel, | 116 | model: ActorImageModel, |
117 | as: 'Banner', | 117 | as: 'Banners', |
118 | required: false | 118 | required: false |
119 | } | 119 | } |
120 | ] | 120 | ] |
@@ -153,9 +153,6 @@ export const unusedActorAttributesForAPI = [ | |||
153 | fields: [ 'serverId' ] | 153 | fields: [ 'serverId' ] |
154 | }, | 154 | }, |
155 | { | 155 | { |
156 | fields: [ 'avatarId' ] | ||
157 | }, | ||
158 | { | ||
159 | fields: [ 'followersUrl' ] | 156 | fields: [ 'followersUrl' ] |
160 | } | 157 | } |
161 | ] | 158 | ] |
@@ -231,35 +228,31 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
231 | @UpdatedAt | 228 | @UpdatedAt |
232 | updatedAt: Date | 229 | updatedAt: Date |
233 | 230 | ||
234 | @ForeignKey(() => ActorImageModel) | 231 | @HasMany(() => ActorImageModel, { |
235 | @Column | 232 | as: 'Avatars', |
236 | avatarId: number | 233 | onDelete: 'cascade', |
237 | 234 | hooks: true, | |
238 | @ForeignKey(() => ActorImageModel) | ||
239 | @Column | ||
240 | bannerId: number | ||
241 | |||
242 | @BelongsTo(() => ActorImageModel, { | ||
243 | foreignKey: { | 235 | foreignKey: { |
244 | name: 'avatarId', | 236 | allowNull: false |
245 | allowNull: true | ||
246 | }, | 237 | }, |
247 | as: 'Avatar', | 238 | scope: { |
248 | onDelete: 'set null', | 239 | type: ActorImageType.AVATAR |
249 | hooks: true | 240 | } |
250 | }) | 241 | }) |
251 | Avatar: ActorImageModel | 242 | Avatars: ActorImageModel[] |
252 | 243 | ||
253 | @BelongsTo(() => ActorImageModel, { | 244 | @HasMany(() => ActorImageModel, { |
245 | as: 'Banners', | ||
246 | onDelete: 'cascade', | ||
247 | hooks: true, | ||
254 | foreignKey: { | 248 | foreignKey: { |
255 | name: 'bannerId', | 249 | allowNull: false |
256 | allowNull: true | ||
257 | }, | 250 | }, |
258 | as: 'Banner', | 251 | scope: { |
259 | onDelete: 'set null', | 252 | type: ActorImageType.BANNER |
260 | hooks: true | 253 | } |
261 | }) | 254 | }) |
262 | Banner: ActorImageModel | 255 | Banners: ActorImageModel[] |
263 | 256 | ||
264 | @HasMany(() => ActorFollowModel, { | 257 | @HasMany(() => ActorFollowModel, { |
265 | foreignKey: { | 258 | foreignKey: { |
@@ -386,8 +379,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
386 | transaction | 379 | transaction |
387 | } | 380 | } |
388 | 381 | ||
389 | return ActorModel.scope(ScopeNames.FULL) | 382 | return ActorModel.scope(ScopeNames.FULL).findOne(query) |
390 | .findOne(query) | ||
391 | } | 383 | } |
392 | 384 | ||
393 | return ModelCache.Instance.doCache({ | 385 | return ModelCache.Instance.doCache({ |
@@ -410,8 +402,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
410 | transaction | 402 | transaction |
411 | } | 403 | } |
412 | 404 | ||
413 | return ActorModel.unscoped() | 405 | return ActorModel.unscoped().findOne(query) |
414 | .findOne(query) | ||
415 | } | 406 | } |
416 | 407 | ||
417 | return ModelCache.Instance.doCache({ | 408 | return ModelCache.Instance.doCache({ |
@@ -532,55 +523,50 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
532 | } | 523 | } |
533 | 524 | ||
534 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 525 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
535 | let avatar: ActorImage = null | ||
536 | if (this.Avatar) { | ||
537 | avatar = this.Avatar.toFormattedJSON() | ||
538 | } | ||
539 | |||
540 | return { | 526 | return { |
541 | url: this.url, | 527 | url: this.url, |
542 | name: this.preferredUsername, | 528 | name: this.preferredUsername, |
543 | host: this.getHost(), | 529 | host: this.getHost(), |
544 | avatar | 530 | avatars: (this.Avatars || []).map(a => a.toFormattedJSON()), |
531 | |||
532 | // TODO: remove, deprecated in 4.2 | ||
533 | avatar: this.hasImage(ActorImageType.AVATAR) | ||
534 | ? this.Avatars[0].toFormattedJSON() | ||
535 | : undefined | ||
545 | } | 536 | } |
546 | } | 537 | } |
547 | 538 | ||
548 | toFormattedJSON (this: MActorFormattable) { | 539 | toFormattedJSON (this: MActorFormattable) { |
549 | const base = this.toFormattedSummaryJSON() | 540 | return { |
550 | 541 | ...this.toFormattedSummaryJSON(), | |
551 | let banner: ActorImage = null | ||
552 | if (this.Banner) { | ||
553 | banner = this.Banner.toFormattedJSON() | ||
554 | } | ||
555 | 542 | ||
556 | return Object.assign(base, { | ||
557 | id: this.id, | 543 | id: this.id, |
558 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 544 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
559 | followingCount: this.followingCount, | 545 | followingCount: this.followingCount, |
560 | followersCount: this.followersCount, | 546 | followersCount: this.followersCount, |
561 | banner, | 547 | createdAt: this.getCreatedAt(), |
562 | createdAt: this.getCreatedAt() | 548 | |
563 | }) | 549 | banners: (this.Banners || []).map(b => b.toFormattedJSON()), |
550 | |||
551 | // TODO: remove, deprecated in 4.2 | ||
552 | banner: this.hasImage(ActorImageType.BANNER) | ||
553 | ? this.Banners[0].toFormattedJSON() | ||
554 | : undefined | ||
555 | } | ||
564 | } | 556 | } |
565 | 557 | ||
566 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { | 558 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
567 | let icon: ActivityIconObject | 559 | let icon: ActivityIconObject |
560 | let icons: ActivityIconObject[] | ||
568 | let image: ActivityIconObject | 561 | let image: ActivityIconObject |
569 | 562 | ||
570 | if (this.avatarId) { | 563 | if (this.hasImage(ActorImageType.AVATAR)) { |
571 | const extension = getLowercaseExtension(this.Avatar.filename) | 564 | icon = getBiggestActorImage(this.Avatars).toActivityPubObject() |
572 | 565 | icons = this.Avatars.map(a => a.toActivityPubObject()) | |
573 | icon = { | ||
574 | type: 'Image', | ||
575 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
576 | height: this.Avatar.height, | ||
577 | width: this.Avatar.width, | ||
578 | url: this.getAvatarUrl() | ||
579 | } | ||
580 | } | 566 | } |
581 | 567 | ||
582 | if (this.bannerId) { | 568 | if (this.hasImage(ActorImageType.BANNER)) { |
583 | const banner = (this as MActorAPChannel).Banner | 569 | const banner = getBiggestActorImage((this as MActorAPChannel).Banners) |
584 | const extension = getLowercaseExtension(banner.filename) | 570 | const extension = getLowercaseExtension(banner.filename) |
585 | 571 | ||
586 | image = { | 572 | image = { |
@@ -588,7 +574,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
588 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | 574 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
589 | height: banner.height, | 575 | height: banner.height, |
590 | width: banner.width, | 576 | width: banner.width, |
591 | url: this.getBannerUrl() | 577 | url: ActorImageModel.getImageUrl(banner) |
592 | } | 578 | } |
593 | } | 579 | } |
594 | 580 | ||
@@ -612,7 +598,10 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
612 | publicKeyPem: this.publicKey | 598 | publicKeyPem: this.publicKey |
613 | }, | 599 | }, |
614 | published: this.getCreatedAt().toISOString(), | 600 | published: this.getCreatedAt().toISOString(), |
601 | |||
615 | icon, | 602 | icon, |
603 | icons, | ||
604 | |||
616 | image | 605 | image |
617 | } | 606 | } |
618 | 607 | ||
@@ -677,16 +666,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
677 | return this.Server ? this.Server.redundancyAllowed : false | 666 | return this.Server ? this.Server.redundancyAllowed : false |
678 | } | 667 | } |
679 | 668 | ||
680 | getAvatarUrl () { | 669 | hasImage (type: ActorImageType) { |
681 | if (!this.avatarId) return undefined | 670 | const images = type === ActorImageType.AVATAR |
682 | 671 | ? this.Avatars | |
683 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 672 | : this.Banners |
684 | } | ||
685 | |||
686 | getBannerUrl () { | ||
687 | if (!this.bannerId) return undefined | ||
688 | 673 | ||
689 | return WEBSERVER.URL + this.Banner.getStaticPath() | 674 | return Array.isArray(images) && images.length !== 0 |
690 | } | 675 | } |
691 | 676 | ||
692 | isOutdated () { | 677 | isOutdated () { |