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 | |
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')
31 files changed, 1167 insertions, 757 deletions
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 6a441a210..d9eb25f0f 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | 2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' |
3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | 3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { AbuseMessage } from '@shared/models' | 4 | import { AbuseMessage } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
7 | import { getSort, throwIfNotValid } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
8 | import { AbuseModel } from './abuse' | 8 | import { AbuseModel } from './abuse' |
9 | import { FindOptions } from 'sequelize/dist' | ||
9 | 10 | ||
10 | @Table({ | 11 | @Table({ |
11 | tableName: 'abuseMessage', | 12 | tableName: 'abuseMessage', |
@@ -62,21 +63,28 @@ export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessage | |||
62 | Abuse: AbuseModel | 63 | Abuse: AbuseModel |
63 | 64 | ||
64 | static listForApi (abuseId: number) { | 65 | static listForApi (abuseId: number) { |
65 | const options = { | 66 | const getQuery = (forCount: boolean) => { |
66 | where: { abuseId }, | 67 | const query: FindOptions = { |
68 | where: { abuseId }, | ||
69 | order: getSort('createdAt') | ||
70 | } | ||
67 | 71 | ||
68 | order: getSort('createdAt'), | 72 | if (forCount !== true) { |
73 | query.include = [ | ||
74 | { | ||
75 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
76 | required: false | ||
77 | } | ||
78 | ] | ||
79 | } | ||
69 | 80 | ||
70 | include: [ | 81 | return query |
71 | { | ||
72 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
73 | required: false | ||
74 | } | ||
75 | ] | ||
76 | } | 82 | } |
77 | 83 | ||
78 | return AbuseMessageModel.findAndCountAll(options) | 84 | return Promise.all([ |
79 | .then(({ rows, count }) => ({ data: rows, total: count })) | 85 | AbuseMessageModel.count(getQuery(true)), |
86 | AbuseMessageModel.findAll(getQuery(false)) | ||
87 | ]).then(([ total, data ]) => ({ total, data })) | ||
80 | } | 88 | } |
81 | 89 | ||
82 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { | 90 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 1162962bf..a7b8db076 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { FindOptions, Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | 3 | import { handlesToNameAndHost } from '@server/helpers/actors' |
4 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 4 | import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
@@ -9,27 +9,6 @@ import { ServerModel } from '../server/server' | |||
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | enum ScopeNames { | ||
13 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | ||
14 | } | ||
15 | |||
16 | @Scopes(() => ({ | ||
17 | [ScopeNames.WITH_ACCOUNTS]: { | ||
18 | include: [ | ||
19 | { | ||
20 | model: AccountModel, | ||
21 | required: true, | ||
22 | as: 'ByAccount' | ||
23 | }, | ||
24 | { | ||
25 | model: AccountModel, | ||
26 | required: true, | ||
27 | as: 'BlockedAccount' | ||
28 | } | ||
29 | ] | ||
30 | } | ||
31 | })) | ||
32 | |||
33 | @Table({ | 12 | @Table({ |
34 | tableName: 'accountBlocklist', | 13 | tableName: 'accountBlocklist', |
35 | indexes: [ | 14 | indexes: [ |
@@ -123,33 +102,45 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB | |||
123 | }) { | 102 | }) { |
124 | const { start, count, sort, search, accountId } = parameters | 103 | const { start, count, sort, search, accountId } = parameters |
125 | 104 | ||
126 | const query = { | 105 | const getQuery = (forCount: boolean) => { |
127 | offset: start, | 106 | const query: FindOptions = { |
128 | limit: count, | 107 | offset: start, |
129 | order: getSort(sort) | 108 | limit: count, |
130 | } | 109 | order: getSort(sort), |
110 | where: { accountId } | ||
111 | } | ||
131 | 112 | ||
132 | const where = { | 113 | if (search) { |
133 | accountId | 114 | Object.assign(query.where, { |
134 | } | 115 | [Op.or]: [ |
116 | searchAttribute(search, '$BlockedAccount.name$'), | ||
117 | searchAttribute(search, '$BlockedAccount.Actor.url$') | ||
118 | ] | ||
119 | }) | ||
120 | } | ||
135 | 121 | ||
136 | if (search) { | 122 | if (forCount !== true) { |
137 | Object.assign(where, { | 123 | query.include = [ |
138 | [Op.or]: [ | 124 | { |
139 | searchAttribute(search, '$BlockedAccount.name$'), | 125 | model: AccountModel, |
140 | searchAttribute(search, '$BlockedAccount.Actor.url$') | 126 | required: true, |
127 | as: 'ByAccount' | ||
128 | }, | ||
129 | { | ||
130 | model: AccountModel, | ||
131 | required: true, | ||
132 | as: 'BlockedAccount' | ||
133 | } | ||
141 | ] | 134 | ] |
142 | }) | 135 | } |
143 | } | ||
144 | 136 | ||
145 | Object.assign(query, { where }) | 137 | return query |
138 | } | ||
146 | 139 | ||
147 | return AccountBlocklistModel | 140 | return Promise.all([ |
148 | .scope([ ScopeNames.WITH_ACCOUNTS ]) | 141 | AccountBlocklistModel.count(getQuery(true)), |
149 | .findAndCountAll<MAccountBlocklistAccounts>(query) | 142 | AccountBlocklistModel.findAll(getQuery(false)) |
150 | .then(({ rows, count }) => { | 143 | ]).then(([ total, data ]) => ({ total, data })) |
151 | return { total: count, data: rows } | ||
152 | }) | ||
153 | } | 144 | } |
154 | 145 | ||
155 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { | 146 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index e89d31adf..7303651eb 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -121,29 +121,40 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
121 | type?: string | 121 | type?: string |
122 | accountId: number | 122 | accountId: number |
123 | }) { | 123 | }) { |
124 | const query: FindOptions = { | 124 | const getQuery = (forCount: boolean) => { |
125 | offset: options.start, | 125 | const query: FindOptions = { |
126 | limit: options.count, | 126 | offset: options.start, |
127 | order: getSort(options.sort), | 127 | limit: options.count, |
128 | where: { | 128 | order: getSort(options.sort), |
129 | accountId: options.accountId | 129 | where: { |
130 | }, | 130 | accountId: options.accountId |
131 | include: [ | ||
132 | { | ||
133 | model: VideoModel, | ||
134 | required: true, | ||
135 | include: [ | ||
136 | { | ||
137 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
138 | required: true | ||
139 | } | ||
140 | ] | ||
141 | } | 131 | } |
142 | ] | 132 | } |
133 | |||
134 | if (options.type) query.where['type'] = options.type | ||
135 | |||
136 | if (forCount !== true) { | ||
137 | query.include = [ | ||
138 | { | ||
139 | model: VideoModel, | ||
140 | required: true, | ||
141 | include: [ | ||
142 | { | ||
143 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
144 | required: true | ||
145 | } | ||
146 | ] | ||
147 | } | ||
148 | ] | ||
149 | } | ||
150 | |||
151 | return query | ||
143 | } | 152 | } |
144 | if (options.type) query.where['type'] = options.type | ||
145 | 153 | ||
146 | return AccountVideoRateModel.findAndCountAll(query) | 154 | return Promise.all([ |
155 | AccountVideoRateModel.count(getQuery(true)), | ||
156 | AccountVideoRateModel.findAll(getQuery(false)) | ||
157 | ]).then(([ total, data ]) => ({ total, data })) | ||
147 | } | 158 | } |
148 | 159 | ||
149 | static listRemoteRateUrlsOfLocalVideos () { | 160 | static listRemoteRateUrlsOfLocalVideos () { |
@@ -232,7 +243,10 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
232 | ] | 243 | ] |
233 | } | 244 | } |
234 | 245 | ||
235 | return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query) | 246 | return Promise.all([ |
247 | AccountVideoRateModel.count(query), | ||
248 | AccountVideoRateModel.findAll<MAccountVideoRateAccountUrl>(query) | ||
249 | ]).then(([ total, data ]) => ({ total, data })) | ||
236 | } | 250 | } |
237 | 251 | ||
238 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { | 252 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 619a598dd..8a7dfba94 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -54,6 +54,7 @@ export type SummaryOptions = { | |||
54 | whereActor?: WhereOptions | 54 | whereActor?: WhereOptions |
55 | whereServer?: WhereOptions | 55 | whereServer?: WhereOptions |
56 | withAccountBlockerIds?: number[] | 56 | withAccountBlockerIds?: number[] |
57 | forCount?: boolean | ||
57 | } | 58 | } |
58 | 59 | ||
59 | @DefaultScope(() => ({ | 60 | @DefaultScope(() => ({ |
@@ -73,22 +74,24 @@ export type SummaryOptions = { | |||
73 | where: options.whereServer | 74 | where: options.whereServer |
74 | } | 75 | } |
75 | 76 | ||
76 | const queryInclude: Includeable[] = [ | 77 | const actorInclude: Includeable = { |
77 | { | 78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 79 | model: ActorModel.unscoped(), |
79 | model: ActorModel.unscoped(), | 80 | required: options.actorRequired ?? true, |
80 | required: options.actorRequired ?? true, | 81 | where: options.whereActor, |
81 | where: options.whereActor, | 82 | include: [ serverInclude ] |
82 | include: [ | 83 | } |
83 | serverInclude, | ||
84 | 84 | ||
85 | { | 85 | if (options.forCount !== true) { |
86 | model: ActorImageModel.unscoped(), | 86 | actorInclude.include.push({ |
87 | as: 'Avatar', | 87 | model: ActorImageModel, |
88 | required: false | 88 | as: 'Avatars', |
89 | } | 89 | required: false |
90 | ] | 90 | }) |
91 | } | 91 | } |
92 | |||
93 | const queryInclude: Includeable[] = [ | ||
94 | actorInclude | ||
92 | ] | 95 | ] |
93 | 96 | ||
94 | const query: FindOptions = { | 97 | const query: FindOptions = { |
@@ -349,13 +352,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
349 | order: getSort(sort) | 352 | order: getSort(sort) |
350 | } | 353 | } |
351 | 354 | ||
352 | return AccountModel.findAndCountAll(query) | 355 | return Promise.all([ |
353 | .then(({ rows, count }) => { | 356 | AccountModel.count(), |
354 | return { | 357 | AccountModel.findAll(query) |
355 | data: rows, | 358 | ]).then(([ total, data ]) => ({ total, data })) |
356 | total: count | ||
357 | } | ||
358 | }) | ||
359 | } | 359 | } |
360 | 360 | ||
361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { | 361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { |
@@ -407,16 +407,15 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
407 | } | 407 | } |
408 | 408 | ||
409 | toFormattedJSON (this: MAccountFormattable): Account { | 409 | toFormattedJSON (this: MAccountFormattable): Account { |
410 | const actor = this.Actor.toFormattedJSON() | 410 | return { |
411 | const account = { | 411 | ...this.Actor.toFormattedJSON(), |
412 | |||
412 | id: this.id, | 413 | id: this.id, |
413 | displayName: this.getDisplayName(), | 414 | displayName: this.getDisplayName(), |
414 | description: this.description, | 415 | description: this.description, |
415 | updatedAt: this.updatedAt, | 416 | updatedAt: this.updatedAt, |
416 | userId: this.userId ? this.userId : undefined | 417 | userId: this.userId ?? undefined |
417 | } | 418 | } |
418 | |||
419 | return Object.assign(actor, account) | ||
420 | } | 419 | } |
421 | 420 | ||
422 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { | 421 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { |
@@ -424,10 +423,14 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
424 | 423 | ||
425 | return { | 424 | return { |
426 | id: this.id, | 425 | id: this.id, |
427 | name: actor.name, | ||
428 | displayName: this.getDisplayName(), | 426 | displayName: this.getDisplayName(), |
427 | |||
428 | name: actor.name, | ||
429 | url: actor.url, | 429 | url: actor.url, |
430 | host: actor.host, | 430 | host: actor.host, |
431 | avatars: actor.avatars, | ||
432 | |||
433 | // TODO: remove, deprecated in 4.2 | ||
431 | avatar: actor.avatar | 434 | avatar: actor.avatar |
432 | } | 435 | } |
433 | } | 436 | } |
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 () { |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 05083e3f7..fa5b4cc4b 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -239,11 +239,10 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> { | |||
239 | 239 | ||
240 | if (options.pluginType) query.where['type'] = options.pluginType | 240 | if (options.pluginType) query.where['type'] = options.pluginType |
241 | 241 | ||
242 | return PluginModel | 242 | return Promise.all([ |
243 | .findAndCountAll<MPlugin>(query) | 243 | PluginModel.count(query), |
244 | .then(({ rows, count }) => { | 244 | PluginModel.findAll<MPlugin>(query) |
245 | return { total: count, data: rows } | 245 | ]).then(([ total, data ]) => ({ total, data })) |
246 | }) | ||
247 | } | 246 | } |
248 | 247 | ||
249 | static listInstalled (): Promise<MPlugin[]> { | 248 | static listInstalled (): Promise<MPlugin[]> { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9f64eeb7f..9752dfbc3 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | 3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
@@ -169,16 +169,15 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo | |||
169 | order: getSort(sort), | 169 | order: getSort(sort), |
170 | where: { | 170 | where: { |
171 | accountId, | 171 | accountId, |
172 | |||
172 | ...searchAttribute(search, '$BlockedServer.host$') | 173 | ...searchAttribute(search, '$BlockedServer.host$') |
173 | } | 174 | } |
174 | } | 175 | } |
175 | 176 | ||
176 | return ServerBlocklistModel | 177 | return Promise.all([ |
177 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) | 178 | ServerBlocklistModel.scope(ScopeNames.WITH_SERVER).count(query), |
178 | .findAndCountAll<MServerBlocklistAccountServer>(query) | 179 | ServerBlocklistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]).findAll<MServerBlocklistAccountServer>(query) |
179 | .then(({ rows, count }) => { | 180 | ]).then(([ total, data ]) => ({ total, data })) |
180 | return { total: count, data: rows } | ||
181 | }) | ||
182 | } | 181 | } |
183 | 182 | ||
184 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { | 183 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 5b97510e0..802404555 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './model-builder' | ||
1 | export * from './query' | 2 | export * from './query' |
2 | export * from './update' | 3 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts new file mode 100644 index 000000000..c015ca4f5 --- /dev/null +++ b/server/models/shared/model-builder.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import { isPlainObject } from 'lodash' | ||
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | ||
6 | private readonly modelRegistry = new Map<string, T>() | ||
7 | |||
8 | constructor (private readonly sequelize: Sequelize) { | ||
9 | |||
10 | } | ||
11 | |||
12 | createModels (jsonArray: any[], baseModelName: string): T[] { | ||
13 | const result: T[] = [] | ||
14 | |||
15 | for (const json of jsonArray) { | ||
16 | const { created, model } = this.createModel(json, baseModelName, json.id + '.' + baseModelName) | ||
17 | |||
18 | if (created) result.push(model) | ||
19 | } | ||
20 | |||
21 | return result | ||
22 | } | ||
23 | |||
24 | private createModel (json: any, modelName: string, keyPath: string) { | ||
25 | if (!json.id) return { created: false, model: null } | ||
26 | |||
27 | const { created, model } = this.createOrFindModel(json, modelName, keyPath) | ||
28 | |||
29 | for (const key of Object.keys(json)) { | ||
30 | const value = json[key] | ||
31 | if (!value) continue | ||
32 | |||
33 | // Child model | ||
34 | if (isPlainObject(value)) { | ||
35 | const { created, model: subModel } = this.createModel(value, key, keyPath + '.' + json.id + '.' + key) | ||
36 | if (!created || !subModel) continue | ||
37 | |||
38 | const Model = this.findModelBuilder(modelName) | ||
39 | const association = Model.associations[key] | ||
40 | |||
41 | if (!association) { | ||
42 | logger.error('Cannot find association %s of model %s', key, modelName, { associations: Object.keys(Model.associations) }) | ||
43 | continue | ||
44 | } | ||
45 | |||
46 | if (association.isMultiAssociation) { | ||
47 | if (!Array.isArray(model[key])) model[key] = [] | ||
48 | |||
49 | model[key].push(subModel) | ||
50 | } else { | ||
51 | model[key] = subModel | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | return { created, model } | ||
57 | } | ||
58 | |||
59 | private createOrFindModel (json: any, modelName: string, keyPath: string) { | ||
60 | const registryKey = this.getModelRegistryKey(json, keyPath) | ||
61 | if (this.modelRegistry.has(registryKey)) { | ||
62 | return { | ||
63 | created: false, | ||
64 | model: this.modelRegistry.get(registryKey) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | const Model = this.findModelBuilder(modelName) | ||
69 | |||
70 | if (!Model) { | ||
71 | logger.error( | ||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | ||
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | ||
74 | ) | ||
75 | return undefined | ||
76 | } | ||
77 | |||
78 | // FIXME: typings | ||
79 | const model = new (Model as any)(json) | ||
80 | this.modelRegistry.set(registryKey, model) | ||
81 | |||
82 | return { created: true, model } | ||
83 | } | ||
84 | |||
85 | private findModelBuilder (modelName: string) { | ||
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | ||
87 | } | ||
88 | |||
89 | private buildSequelizeModelName (modelName: string) { | ||
90 | if (modelName === 'Avatars') return 'ActorImageModel' | ||
91 | if (modelName === 'ActorFollowing') return 'ActorModel' | ||
92 | if (modelName === 'ActorFollower') return 'ActorModel' | ||
93 | if (modelName === 'FlaggedAccount') return 'AccountModel' | ||
94 | |||
95 | return modelName + 'Model' | ||
96 | } | ||
97 | |||
98 | private getModelRegistryKey (json: any, keyPath: string) { | ||
99 | return keyPath + json.id | ||
100 | } | ||
101 | } | ||
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts new file mode 100644 index 000000000..9eae4fc22 --- /dev/null +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -0,0 +1,269 @@ | |||
1 | import { QueryTypes, Sequelize } from 'sequelize' | ||
2 | import { ModelBuilder } from '@server/models/shared' | ||
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | |||
7 | export interface ListNotificationsOptions { | ||
8 | userId: number | ||
9 | unread?: boolean | ||
10 | sort: string | ||
11 | offset: number | ||
12 | limit: number | ||
13 | sequelize: Sequelize | ||
14 | } | ||
15 | |||
16 | export class UserNotificationListQueryBuilder { | ||
17 | private innerQuery: string | ||
18 | private replacements: any = {} | ||
19 | private query: string | ||
20 | |||
21 | constructor (private readonly options: ListNotificationsOptions) { | ||
22 | |||
23 | } | ||
24 | |||
25 | async listNotifications () { | ||
26 | this.buildQuery() | ||
27 | |||
28 | const results = await this.options.sequelize.query(this.query, { | ||
29 | replacements: this.replacements, | ||
30 | type: QueryTypes.SELECT, | ||
31 | nest: true | ||
32 | }) | ||
33 | |||
34 | const modelBuilder = new ModelBuilder<UserNotificationModelForApi>(this.options.sequelize) | ||
35 | |||
36 | return modelBuilder.createModels(results, 'UserNotification') | ||
37 | } | ||
38 | |||
39 | private buildInnerQuery () { | ||
40 | this.innerQuery = `SELECT * FROM "userNotification" AS "UserNotificationModel" ` + | ||
41 | `${this.getWhere()} ` + | ||
42 | `${this.getOrder()} ` + | ||
43 | `LIMIT :limit OFFSET :offset ` | ||
44 | |||
45 | this.replacements.limit = this.options.limit | ||
46 | this.replacements.offset = this.options.offset | ||
47 | } | ||
48 | |||
49 | private buildQuery () { | ||
50 | this.buildInnerQuery() | ||
51 | |||
52 | this.query = ` | ||
53 | ${this.getSelect()} | ||
54 | FROM (${this.innerQuery}) "UserNotificationModel" | ||
55 | ${this.getJoins()} | ||
56 | ${this.getOrder()}` | ||
57 | } | ||
58 | |||
59 | private getWhere () { | ||
60 | let base = '"UserNotificationModel"."userId" = :userId ' | ||
61 | this.replacements.userId = this.options.userId | ||
62 | |||
63 | if (this.options.unread === true) { | ||
64 | base += 'AND "UserNotificationModel"."read" IS FALSE ' | ||
65 | } else if (this.options.unread === false) { | ||
66 | base += 'AND "UserNotificationModel"."read" IS TRUE ' | ||
67 | } | ||
68 | |||
69 | return `WHERE ${base}` | ||
70 | } | ||
71 | |||
72 | private getOrder () { | ||
73 | const orders = getSort(this.options.sort) | ||
74 | |||
75 | return 'ORDER BY ' + orders.map(o => `"UserNotificationModel"."${o[0]}" ${o[1]}`).join(', ') | ||
76 | } | ||
77 | |||
78 | private getSelect () { | ||
79 | return `SELECT | ||
80 | "UserNotificationModel"."id", | ||
81 | "UserNotificationModel"."type", | ||
82 | "UserNotificationModel"."read", | ||
83 | "UserNotificationModel"."createdAt", | ||
84 | "UserNotificationModel"."updatedAt", | ||
85 | "Video"."id" AS "Video.id", | ||
86 | "Video"."uuid" AS "Video.uuid", | ||
87 | "Video"."name" AS "Video.name", | ||
88 | "Video->VideoChannel"."id" AS "Video.VideoChannel.id", | ||
89 | "Video->VideoChannel"."name" AS "Video.VideoChannel.name", | ||
90 | "Video->VideoChannel->Actor"."id" AS "Video.VideoChannel.Actor.id", | ||
91 | "Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername", | ||
92 | "Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id", | ||
93 | "Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width", | ||
94 | "Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename", | ||
95 | "Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id", | ||
96 | "Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host", | ||
97 | "VideoComment"."id" AS "VideoComment.id", | ||
98 | "VideoComment"."originCommentId" AS "VideoComment.originCommentId", | ||
99 | "VideoComment->Account"."id" AS "VideoComment.Account.id", | ||
100 | "VideoComment->Account"."name" AS "VideoComment.Account.name", | ||
101 | "VideoComment->Account->Actor"."id" AS "VideoComment.Account.Actor.id", | ||
102 | "VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername", | ||
103 | "VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id", | ||
104 | "VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width", | ||
105 | "VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename", | ||
106 | "VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id", | ||
107 | "VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host", | ||
108 | "VideoComment->Video"."id" AS "VideoComment.Video.id", | ||
109 | "VideoComment->Video"."uuid" AS "VideoComment.Video.uuid", | ||
110 | "VideoComment->Video"."name" AS "VideoComment.Video.name", | ||
111 | "Abuse"."id" AS "Abuse.id", | ||
112 | "Abuse"."state" AS "Abuse.state", | ||
113 | "Abuse->VideoAbuse"."id" AS "Abuse.VideoAbuse.id", | ||
114 | "Abuse->VideoAbuse->Video"."id" AS "Abuse.VideoAbuse.Video.id", | ||
115 | "Abuse->VideoAbuse->Video"."uuid" AS "Abuse.VideoAbuse.Video.uuid", | ||
116 | "Abuse->VideoAbuse->Video"."name" AS "Abuse.VideoAbuse.Video.name", | ||
117 | "Abuse->VideoCommentAbuse"."id" AS "Abuse.VideoCommentAbuse.id", | ||
118 | "Abuse->VideoCommentAbuse->VideoComment"."id" AS "Abuse.VideoCommentAbuse.VideoComment.id", | ||
119 | "Abuse->VideoCommentAbuse->VideoComment"."originCommentId" AS "Abuse.VideoCommentAbuse.VideoComment.originCommentId", | ||
120 | "Abuse->VideoCommentAbuse->VideoComment->Video"."id" AS "Abuse.VideoCommentAbuse.VideoComment.Video.id", | ||
121 | "Abuse->VideoCommentAbuse->VideoComment->Video"."name" AS "Abuse.VideoCommentAbuse.VideoComment.Video.name", | ||
122 | "Abuse->VideoCommentAbuse->VideoComment->Video"."uuid" AS "Abuse.VideoCommentAbuse.VideoComment.Video.uuid", | ||
123 | "Abuse->FlaggedAccount"."id" AS "Abuse.FlaggedAccount.id", | ||
124 | "Abuse->FlaggedAccount"."name" AS "Abuse.FlaggedAccount.name", | ||
125 | "Abuse->FlaggedAccount"."description" AS "Abuse.FlaggedAccount.description", | ||
126 | "Abuse->FlaggedAccount"."actorId" AS "Abuse.FlaggedAccount.actorId", | ||
127 | "Abuse->FlaggedAccount"."userId" AS "Abuse.FlaggedAccount.userId", | ||
128 | "Abuse->FlaggedAccount"."applicationId" AS "Abuse.FlaggedAccount.applicationId", | ||
129 | "Abuse->FlaggedAccount"."createdAt" AS "Abuse.FlaggedAccount.createdAt", | ||
130 | "Abuse->FlaggedAccount"."updatedAt" AS "Abuse.FlaggedAccount.updatedAt", | ||
131 | "Abuse->FlaggedAccount->Actor"."id" AS "Abuse.FlaggedAccount.Actor.id", | ||
132 | "Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername", | ||
133 | "Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id", | ||
134 | "Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width", | ||
135 | "Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename", | ||
136 | "Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id", | ||
137 | "Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host", | ||
138 | "VideoBlacklist"."id" AS "VideoBlacklist.id", | ||
139 | "VideoBlacklist->Video"."id" AS "VideoBlacklist.Video.id", | ||
140 | "VideoBlacklist->Video"."uuid" AS "VideoBlacklist.Video.uuid", | ||
141 | "VideoBlacklist->Video"."name" AS "VideoBlacklist.Video.name", | ||
142 | "VideoImport"."id" AS "VideoImport.id", | ||
143 | "VideoImport"."magnetUri" AS "VideoImport.magnetUri", | ||
144 | "VideoImport"."targetUrl" AS "VideoImport.targetUrl", | ||
145 | "VideoImport"."torrentName" AS "VideoImport.torrentName", | ||
146 | "VideoImport->Video"."id" AS "VideoImport.Video.id", | ||
147 | "VideoImport->Video"."uuid" AS "VideoImport.Video.uuid", | ||
148 | "VideoImport->Video"."name" AS "VideoImport.Video.name", | ||
149 | "Plugin"."id" AS "Plugin.id", | ||
150 | "Plugin"."name" AS "Plugin.name", | ||
151 | "Plugin"."type" AS "Plugin.type", | ||
152 | "Plugin"."latestVersion" AS "Plugin.latestVersion", | ||
153 | "Application"."id" AS "Application.id", | ||
154 | "Application"."latestPeerTubeVersion" AS "Application.latestPeerTubeVersion", | ||
155 | "ActorFollow"."id" AS "ActorFollow.id", | ||
156 | "ActorFollow"."state" AS "ActorFollow.state", | ||
157 | "ActorFollow->ActorFollower"."id" AS "ActorFollow.ActorFollower.id", | ||
158 | "ActorFollow->ActorFollower"."preferredUsername" AS "ActorFollow.ActorFollower.preferredUsername", | ||
159 | "ActorFollow->ActorFollower->Account"."id" AS "ActorFollow.ActorFollower.Account.id", | ||
160 | "ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name", | ||
161 | "ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id", | ||
162 | "ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width", | ||
163 | "ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename", | ||
164 | "ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id", | ||
165 | "ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host", | ||
166 | "ActorFollow->ActorFollowing"."id" AS "ActorFollow.ActorFollowing.id", | ||
167 | "ActorFollow->ActorFollowing"."preferredUsername" AS "ActorFollow.ActorFollowing.preferredUsername", | ||
168 | "ActorFollow->ActorFollowing"."type" AS "ActorFollow.ActorFollowing.type", | ||
169 | "ActorFollow->ActorFollowing->VideoChannel"."id" AS "ActorFollow.ActorFollowing.VideoChannel.id", | ||
170 | "ActorFollow->ActorFollowing->VideoChannel"."name" AS "ActorFollow.ActorFollowing.VideoChannel.name", | ||
171 | "ActorFollow->ActorFollowing->Account"."id" AS "ActorFollow.ActorFollowing.Account.id", | ||
172 | "ActorFollow->ActorFollowing->Account"."name" AS "ActorFollow.ActorFollowing.Account.name", | ||
173 | "ActorFollow->ActorFollowing->Server"."id" AS "ActorFollow.ActorFollowing.Server.id", | ||
174 | "ActorFollow->ActorFollowing->Server"."host" AS "ActorFollow.ActorFollowing.Server.host", | ||
175 | "Account"."id" AS "Account.id", | ||
176 | "Account"."name" AS "Account.name", | ||
177 | "Account->Actor"."id" AS "Account.Actor.id", | ||
178 | "Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername", | ||
179 | "Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id", | ||
180 | "Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width", | ||
181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | ||
182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | ||
183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | ||
184 | } | ||
185 | |||
186 | private getJoins () { | ||
187 | return ` | ||
188 | LEFT JOIN ( | ||
189 | "video" AS "Video" | ||
190 | INNER JOIN "videoChannel" AS "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" | ||
191 | INNER JOIN "actor" AS "Video->VideoChannel->Actor" ON "Video->VideoChannel"."actorId" = "Video->VideoChannel->Actor"."id" | ||
192 | LEFT JOIN "actorImage" AS "Video->VideoChannel->Actor->Avatars" | ||
193 | ON "Video->VideoChannel->Actor"."id" = "Video->VideoChannel->Actor->Avatars"."actorId" | ||
194 | AND "Video->VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
195 | LEFT JOIN "server" AS "Video->VideoChannel->Actor->Server" | ||
196 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" | ||
197 | ) ON "UserNotificationModel"."videoId" = "Video"."id" | ||
198 | |||
199 | LEFT JOIN ( | ||
200 | "videoComment" AS "VideoComment" | ||
201 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" | ||
202 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" | ||
203 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" | ||
204 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" | ||
205 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
206 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" | ||
207 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" | ||
208 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" | ||
209 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" | ||
210 | |||
211 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | ||
212 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | ||
213 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | ||
214 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | ||
215 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
216 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
217 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
218 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
219 | LEFT JOIN ( | ||
220 | "account" AS "Abuse->FlaggedAccount" | ||
221 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
222 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
223 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
224 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
225 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
226 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
227 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
228 | |||
229 | LEFT JOIN ( | ||
230 | "videoBlacklist" AS "VideoBlacklist" | ||
231 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" | ||
232 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" | ||
233 | |||
234 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" | ||
235 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" | ||
236 | |||
237 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" | ||
238 | |||
239 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" | ||
240 | |||
241 | LEFT JOIN ( | ||
242 | "actorFollow" AS "ActorFollow" | ||
243 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | ||
244 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | ||
245 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | ||
246 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | ||
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | ||
248 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | ||
249 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
250 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
251 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
252 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
253 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
254 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
256 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
257 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
258 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
259 | |||
260 | LEFT JOIN ( | ||
261 | "account" AS "Account" | ||
262 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" | ||
263 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" | ||
264 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" | ||
265 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
266 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" | ||
267 | ) ON "UserNotificationModel"."accountId" = "Account"."id"` | ||
268 | } | ||
269 | } | ||
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index edad10a55..eca127e7e 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 4 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { uuidToShort } from '@shared/extra-utils' | 5 | import { uuidToShort } from '@shared/extra-utils' |
5 | import { UserNotification, UserNotificationType } from '@shared/models' | 6 | import { UserNotification, UserNotificationType } from '@shared/models' |
@@ -7,207 +8,18 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
7 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 8 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
8 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 9 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
9 | import { AbuseModel } from '../abuse/abuse' | 10 | import { AbuseModel } from '../abuse/abuse' |
10 | import { VideoAbuseModel } from '../abuse/video-abuse' | ||
11 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
12 | import { AccountModel } from '../account/account' | 11 | import { AccountModel } from '../account/account' |
13 | import { ActorModel } from '../actor/actor' | ||
14 | import { ActorFollowModel } from '../actor/actor-follow' | 12 | import { ActorFollowModel } from '../actor/actor-follow' |
15 | import { ActorImageModel } from '../actor/actor-image' | ||
16 | import { ApplicationModel } from '../application/application' | 13 | import { ApplicationModel } from '../application/application' |
17 | import { PluginModel } from '../server/plugin' | 14 | import { PluginModel } from '../server/plugin' |
18 | import { ServerModel } from '../server/server' | 15 | import { throwIfNotValid } from '../utils' |
19 | import { getSort, throwIfNotValid } from '../utils' | ||
20 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
21 | import { VideoBlacklistModel } from '../video/video-blacklist' | 17 | import { VideoBlacklistModel } from '../video/video-blacklist' |
22 | import { VideoChannelModel } from '../video/video-channel' | ||
23 | import { VideoCommentModel } from '../video/video-comment' | 18 | import { VideoCommentModel } from '../video/video-comment' |
24 | import { VideoImportModel } from '../video/video-import' | 19 | import { VideoImportModel } from '../video/video-import' |
20 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' | ||
25 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
26 | 22 | ||
27 | enum ScopeNames { | ||
28 | WITH_ALL = 'WITH_ALL' | ||
29 | } | ||
30 | |||
31 | function buildActorWithAvatarInclude () { | ||
32 | return { | ||
33 | attributes: [ 'preferredUsername' ], | ||
34 | model: ActorModel.unscoped(), | ||
35 | required: true, | ||
36 | include: [ | ||
37 | { | ||
38 | attributes: [ 'filename' ], | ||
39 | as: 'Avatar', | ||
40 | model: ActorImageModel.unscoped(), | ||
41 | required: false | ||
42 | }, | ||
43 | { | ||
44 | attributes: [ 'host' ], | ||
45 | model: ServerModel.unscoped(), | ||
46 | required: false | ||
47 | } | ||
48 | ] | ||
49 | } | ||
50 | } | ||
51 | |||
52 | function buildVideoInclude (required: boolean) { | ||
53 | return { | ||
54 | attributes: [ 'id', 'uuid', 'name' ], | ||
55 | model: VideoModel.unscoped(), | ||
56 | required | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function buildChannelInclude (required: boolean, withActor = false) { | ||
61 | return { | ||
62 | required, | ||
63 | attributes: [ 'id', 'name' ], | ||
64 | model: VideoChannelModel.unscoped(), | ||
65 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
66 | } | ||
67 | } | ||
68 | |||
69 | function buildAccountInclude (required: boolean, withActor = false) { | ||
70 | return { | ||
71 | required, | ||
72 | attributes: [ 'id', 'name' ], | ||
73 | model: AccountModel.unscoped(), | ||
74 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
75 | } | ||
76 | } | ||
77 | |||
78 | @Scopes(() => ({ | ||
79 | [ScopeNames.WITH_ALL]: { | ||
80 | include: [ | ||
81 | Object.assign(buildVideoInclude(false), { | ||
82 | include: [ buildChannelInclude(true, true) ] | ||
83 | }), | ||
84 | |||
85 | { | ||
86 | attributes: [ 'id', 'originCommentId' ], | ||
87 | model: VideoCommentModel.unscoped(), | ||
88 | required: false, | ||
89 | include: [ | ||
90 | buildAccountInclude(true, true), | ||
91 | buildVideoInclude(true) | ||
92 | ] | ||
93 | }, | ||
94 | |||
95 | { | ||
96 | attributes: [ 'id', 'state' ], | ||
97 | model: AbuseModel.unscoped(), | ||
98 | required: false, | ||
99 | include: [ | ||
100 | { | ||
101 | attributes: [ 'id' ], | ||
102 | model: VideoAbuseModel.unscoped(), | ||
103 | required: false, | ||
104 | include: [ buildVideoInclude(false) ] | ||
105 | }, | ||
106 | { | ||
107 | attributes: [ 'id' ], | ||
108 | model: VideoCommentAbuseModel.unscoped(), | ||
109 | required: false, | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: [ 'id', 'originCommentId' ], | ||
113 | model: VideoCommentModel.unscoped(), | ||
114 | required: false, | ||
115 | include: [ | ||
116 | { | ||
117 | attributes: [ 'id', 'name', 'uuid' ], | ||
118 | model: VideoModel.unscoped(), | ||
119 | required: false | ||
120 | } | ||
121 | ] | ||
122 | } | ||
123 | ] | ||
124 | }, | ||
125 | { | ||
126 | model: AccountModel, | ||
127 | as: 'FlaggedAccount', | ||
128 | required: false, | ||
129 | include: [ buildActorWithAvatarInclude() ] | ||
130 | } | ||
131 | ] | ||
132 | }, | ||
133 | |||
134 | { | ||
135 | attributes: [ 'id' ], | ||
136 | model: VideoBlacklistModel.unscoped(), | ||
137 | required: false, | ||
138 | include: [ buildVideoInclude(true) ] | ||
139 | }, | ||
140 | |||
141 | { | ||
142 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | ||
143 | model: VideoImportModel.unscoped(), | ||
144 | required: false, | ||
145 | include: [ buildVideoInclude(false) ] | ||
146 | }, | ||
147 | |||
148 | { | ||
149 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
150 | model: PluginModel.unscoped(), | ||
151 | required: false | ||
152 | }, | ||
153 | |||
154 | { | ||
155 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
156 | model: ApplicationModel.unscoped(), | ||
157 | required: false | ||
158 | }, | ||
159 | |||
160 | { | ||
161 | attributes: [ 'id', 'state' ], | ||
162 | model: ActorFollowModel.unscoped(), | ||
163 | required: false, | ||
164 | include: [ | ||
165 | { | ||
166 | attributes: [ 'preferredUsername' ], | ||
167 | model: ActorModel.unscoped(), | ||
168 | required: true, | ||
169 | as: 'ActorFollower', | ||
170 | include: [ | ||
171 | { | ||
172 | attributes: [ 'id', 'name' ], | ||
173 | model: AccountModel.unscoped(), | ||
174 | required: true | ||
175 | }, | ||
176 | { | ||
177 | attributes: [ 'filename' ], | ||
178 | as: 'Avatar', | ||
179 | model: ActorImageModel.unscoped(), | ||
180 | required: false | ||
181 | }, | ||
182 | { | ||
183 | attributes: [ 'host' ], | ||
184 | model: ServerModel.unscoped(), | ||
185 | required: false | ||
186 | } | ||
187 | ] | ||
188 | }, | ||
189 | { | ||
190 | attributes: [ 'preferredUsername', 'type' ], | ||
191 | model: ActorModel.unscoped(), | ||
192 | required: true, | ||
193 | as: 'ActorFollowing', | ||
194 | include: [ | ||
195 | buildChannelInclude(false), | ||
196 | buildAccountInclude(false), | ||
197 | { | ||
198 | attributes: [ 'host' ], | ||
199 | model: ServerModel.unscoped(), | ||
200 | required: false | ||
201 | } | ||
202 | ] | ||
203 | } | ||
204 | ] | ||
205 | }, | ||
206 | |||
207 | buildAccountInclude(false, true) | ||
208 | ] | ||
209 | } | ||
210 | })) | ||
211 | @Table({ | 23 | @Table({ |
212 | tableName: 'userNotification', | 24 | tableName: 'userNotification', |
213 | indexes: [ | 25 | indexes: [ |
@@ -342,7 +154,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
342 | }, | 154 | }, |
343 | onDelete: 'cascade' | 155 | onDelete: 'cascade' |
344 | }) | 156 | }) |
345 | Comment: VideoCommentModel | 157 | VideoComment: VideoCommentModel |
346 | 158 | ||
347 | @ForeignKey(() => AbuseModel) | 159 | @ForeignKey(() => AbuseModel) |
348 | @Column | 160 | @Column |
@@ -431,11 +243,14 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
431 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 243 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
432 | const where = { userId } | 244 | const where = { userId } |
433 | 245 | ||
434 | const query: FindOptions = { | 246 | const query = { |
247 | userId, | ||
248 | unread, | ||
435 | offset: start, | 249 | offset: start, |
436 | limit: count, | 250 | limit: count, |
437 | order: getSort(sort), | 251 | sort, |
438 | where | 252 | where, |
253 | sequelize: this.sequelize | ||
439 | } | 254 | } |
440 | 255 | ||
441 | if (unread !== undefined) query.where['read'] = !unread | 256 | if (unread !== undefined) query.where['read'] = !unread |
@@ -445,8 +260,8 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
445 | .then(count => count || 0), | 260 | .then(count => count || 0), |
446 | 261 | ||
447 | count === 0 | 262 | count === 0 |
448 | ? [] | 263 | ? [] as UserNotificationModelForApi[] |
449 | : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) | 264 | : new UserNotificationListQueryBuilder(query).listNotifications() |
450 | ]).then(([ total, data ]) => ({ total, data })) | 265 | ]).then(([ total, data ]) => ({ total, data })) |
451 | } | 266 | } |
452 | 267 | ||
@@ -524,25 +339,31 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
524 | 339 | ||
525 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { | 340 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { |
526 | const video = this.Video | 341 | const video = this.Video |
527 | ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) | 342 | ? { |
343 | ...this.formatVideo(this.Video), | ||
344 | |||
345 | channel: this.formatActor(this.Video.VideoChannel) | ||
346 | } | ||
528 | : undefined | 347 | : undefined |
529 | 348 | ||
530 | const videoImport = this.VideoImport | 349 | const videoImport = this.VideoImport |
531 | ? { | 350 | ? { |
532 | id: this.VideoImport.id, | 351 | id: this.VideoImport.id, |
533 | video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, | 352 | video: this.VideoImport.Video |
353 | ? this.formatVideo(this.VideoImport.Video) | ||
354 | : undefined, | ||
534 | torrentName: this.VideoImport.torrentName, | 355 | torrentName: this.VideoImport.torrentName, |
535 | magnetUri: this.VideoImport.magnetUri, | 356 | magnetUri: this.VideoImport.magnetUri, |
536 | targetUrl: this.VideoImport.targetUrl | 357 | targetUrl: this.VideoImport.targetUrl |
537 | } | 358 | } |
538 | : undefined | 359 | : undefined |
539 | 360 | ||
540 | const comment = this.Comment | 361 | const comment = this.VideoComment |
541 | ? { | 362 | ? { |
542 | id: this.Comment.id, | 363 | id: this.VideoComment.id, |
543 | threadId: this.Comment.getThreadId(), | 364 | threadId: this.VideoComment.getThreadId(), |
544 | account: this.formatActor(this.Comment.Account), | 365 | account: this.formatActor(this.VideoComment.Account), |
545 | video: this.formatVideo(this.Comment.Video) | 366 | video: this.formatVideo(this.VideoComment.Video) |
546 | } | 367 | } |
547 | : undefined | 368 | : undefined |
548 | 369 | ||
@@ -570,8 +391,9 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
570 | id: this.ActorFollow.ActorFollower.Account.id, | 391 | id: this.ActorFollow.ActorFollower.Account.id, |
571 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | 392 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), |
572 | name: this.ActorFollow.ActorFollower.preferredUsername, | 393 | name: this.ActorFollow.ActorFollower.preferredUsername, |
573 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, | 394 | host: this.ActorFollow.ActorFollower.getHost(), |
574 | host: this.ActorFollow.ActorFollower.getHost() | 395 | |
396 | ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars) | ||
575 | }, | 397 | }, |
576 | following: { | 398 | following: { |
577 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], | 399 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], |
@@ -612,7 +434,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
612 | } | 434 | } |
613 | } | 435 | } |
614 | 436 | ||
615 | formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { | 437 | formatVideo (video: UserNotificationIncludes.VideoInclude) { |
616 | return { | 438 | return { |
617 | id: video.id, | 439 | id: video.id, |
618 | uuid: video.uuid, | 440 | uuid: video.uuid, |
@@ -621,7 +443,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
621 | } | 443 | } |
622 | } | 444 | } |
623 | 445 | ||
624 | formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { | 446 | formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) { |
625 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment | 447 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment |
626 | ? { | 448 | ? { |
627 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 449 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
@@ -637,9 +459,13 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
637 | } | 459 | } |
638 | : undefined | 460 | : undefined |
639 | 461 | ||
640 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 462 | const videoAbuse = abuse.VideoAbuse?.Video |
463 | ? this.formatVideo(abuse.VideoAbuse.Video) | ||
464 | : undefined | ||
641 | 465 | ||
642 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined | 466 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) |
467 | ? this.formatActor(abuse.FlaggedAccount) | ||
468 | : undefined | ||
643 | 469 | ||
644 | return { | 470 | return { |
645 | id: abuse.id, | 471 | id: abuse.id, |
@@ -651,19 +477,32 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
651 | } | 477 | } |
652 | 478 | ||
653 | formatActor ( | 479 | formatActor ( |
654 | this: UserNotificationModelForApi, | ||
655 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor | 480 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor |
656 | ) { | 481 | ) { |
657 | const avatar = accountOrChannel.Actor.Avatar | ||
658 | ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } | ||
659 | : undefined | ||
660 | |||
661 | return { | 482 | return { |
662 | id: accountOrChannel.id, | 483 | id: accountOrChannel.id, |
663 | displayName: accountOrChannel.getDisplayName(), | 484 | displayName: accountOrChannel.getDisplayName(), |
664 | name: accountOrChannel.Actor.preferredUsername, | 485 | name: accountOrChannel.Actor.preferredUsername, |
665 | host: accountOrChannel.Actor.getHost(), | 486 | host: accountOrChannel.Actor.getHost(), |
666 | avatar | 487 | |
488 | ...this.formatAvatars(accountOrChannel.Actor.Avatars) | ||
489 | } | ||
490 | } | ||
491 | |||
492 | formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) { | ||
493 | if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] } | ||
494 | |||
495 | return { | ||
496 | avatar: this.formatAvatar(getBiggestActorImage(avatars)), | ||
497 | |||
498 | avatars: avatars.map(a => this.formatAvatar(a)) | ||
499 | } | ||
500 | } | ||
501 | |||
502 | formatAvatar (a: UserNotificationIncludes.ActorImageInclude) { | ||
503 | return { | ||
504 | path: a.getStaticPath(), | ||
505 | width: a.width | ||
667 | } | 506 | } |
668 | } | 507 | } |
669 | } | 508 | } |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index ad8ce08cb..bcf56dfa1 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -106,7 +106,7 @@ enum ScopeNames { | |||
106 | include: [ | 106 | include: [ |
107 | { | 107 | { |
108 | model: ActorImageModel, | 108 | model: ActorImageModel, |
109 | as: 'Banner', | 109 | as: 'Banners', |
110 | required: false | 110 | required: false |
111 | } | 111 | } |
112 | ] | 112 | ] |
@@ -495,13 +495,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
495 | where | 495 | where |
496 | } | 496 | } |
497 | 497 | ||
498 | return UserModel.findAndCountAll(query) | 498 | return Promise.all([ |
499 | .then(({ rows, count }) => { | 499 | UserModel.unscoped().count(query), |
500 | return { | 500 | UserModel.findAll(query) |
501 | data: rows, | 501 | ]).then(([ total, data ]) => ({ total, data })) |
502 | total: count | ||
503 | } | ||
504 | }) | ||
505 | } | 502 | } |
506 | 503 | ||
507 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { | 504 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { |
diff --git a/server/models/utils.ts b/server/models/utils.ts index 66b653e3d..70bfbdb8b 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -181,7 +181,7 @@ function buildServerIdsFollowedBy (actorId: any) { | |||
181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | 181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + |
182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | 182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + |
183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
184 | ')' | 184 | ')' |
185 | } | 185 | } |
186 | 186 | ||
187 | function buildWhereIdOrUUID (id: number | string) { | 187 | function buildWhereIdOrUUID (id: number | string) { |
diff --git a/server/models/video/sql/video/index.ts b/server/models/video/sql/video/index.ts new file mode 100644 index 000000000..e9132d5e1 --- /dev/null +++ b/server/models/video/sql/video/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './video-model-get-query-builder' | ||
2 | export * from './videos-id-list-query-builder' | ||
3 | export * from './videos-model-list-query-builder' | ||
diff --git a/server/models/video/sql/shared/abstract-run-query.ts b/server/models/video/sql/video/shared/abstract-run-query.ts index 8e7a7642d..8e7a7642d 100644 --- a/server/models/video/sql/shared/abstract-run-query.ts +++ b/server/models/video/sql/video/shared/abstract-run-query.ts | |||
diff --git a/server/models/video/sql/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index a6afb04e4..490e5e6e0 100644 --- a/server/models/video/sql/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { createSafeIn } from '@server/models/utils' | 1 | import { createSafeIn } from '@server/models/utils' |
2 | import { MUserAccountId } from '@server/types/models' | 2 | import { MUserAccountId } from '@server/types/models' |
3 | import { ActorImageType } from '@shared/models' | ||
3 | import validator from 'validator' | 4 | import validator from 'validator' |
4 | import { AbstractRunQuery } from './abstract-run-query' | 5 | import { AbstractRunQuery } from './abstract-run-query' |
5 | import { VideoTableAttributes } from './video-table-attributes' | 6 | import { VideoTableAttributes } from './video-table-attributes' |
@@ -42,8 +43,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
42 | ) | 43 | ) |
43 | 44 | ||
44 | this.addJoin( | 45 | this.addJoin( |
45 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | 46 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' + |
46 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' | 47 | 'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' + |
48 | `AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
47 | ) | 49 | ) |
48 | 50 | ||
49 | this.attributes = { | 51 | this.attributes = { |
@@ -51,7 +53,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
51 | 53 | ||
52 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | 54 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), |
53 | ...this.buildActorInclude('VideoChannel->Actor'), | 55 | ...this.buildActorInclude('VideoChannel->Actor'), |
54 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), | 56 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatars'), |
55 | ...this.buildServerInclude('VideoChannel->Actor->Server') | 57 | ...this.buildServerInclude('VideoChannel->Actor->Server') |
56 | } | 58 | } |
57 | } | 59 | } |
@@ -68,8 +70,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
68 | ) | 70 | ) |
69 | 71 | ||
70 | this.addJoin( | 72 | this.addJoin( |
71 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | 73 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' + |
72 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' | 74 | 'ON "VideoChannel->Account"."actorId"= "VideoChannel->Account->Actor->Avatars"."actorId" ' + |
75 | `AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
73 | ) | 76 | ) |
74 | 77 | ||
75 | this.attributes = { | 78 | this.attributes = { |
@@ -77,7 +80,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
77 | 80 | ||
78 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), | 81 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), |
79 | ...this.buildActorInclude('VideoChannel->Account->Actor'), | 82 | ...this.buildActorInclude('VideoChannel->Account->Actor'), |
80 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), | 83 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'), |
81 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') | 84 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') |
82 | } | 85 | } |
83 | } | 86 | } |
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index 3eb3dc07d..3eb3dc07d 100644 --- a/server/models/video/sql/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 7751d8e68..b1b47b721 100644 --- a/server/models/video/sql/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -9,15 +9,15 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | |||
9 | import { TrackerModel } from '@server/models/server/tracker' | 9 | import { TrackerModel } from '@server/models/server/tracker' |
10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
11 | import { VideoInclude } from '@shared/models' | 11 | import { VideoInclude } from '@shared/models' |
12 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | 12 | import { ScheduleVideoUpdateModel } from '../../../schedule-video-update' |
13 | import { TagModel } from '../../tag' | 13 | import { TagModel } from '../../../tag' |
14 | import { ThumbnailModel } from '../../thumbnail' | 14 | import { ThumbnailModel } from '../../../thumbnail' |
15 | import { VideoModel } from '../../video' | 15 | import { VideoModel } from '../../../video' |
16 | import { VideoBlacklistModel } from '../../video-blacklist' | 16 | import { VideoBlacklistModel } from '../../../video-blacklist' |
17 | import { VideoChannelModel } from '../../video-channel' | 17 | import { VideoChannelModel } from '../../../video-channel' |
18 | import { VideoFileModel } from '../../video-file' | 18 | import { VideoFileModel } from '../../../video-file' |
19 | import { VideoLiveModel } from '../../video-live' | 19 | import { VideoLiveModel } from '../../../video-live' |
20 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | 20 | import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist' |
21 | import { VideoTableAttributes } from './video-table-attributes' | 21 | import { VideoTableAttributes } from './video-table-attributes' |
22 | 22 | ||
23 | type SQLRow = { [id: string]: string | number } | 23 | type SQLRow = { [id: string]: string | number } |
@@ -34,6 +34,7 @@ export class VideoModelBuilder { | |||
34 | private videoFileMemo: { [ id: number ]: VideoFileModel } | 34 | private videoFileMemo: { [ id: number ]: VideoFileModel } |
35 | 35 | ||
36 | private thumbnailsDone: Set<any> | 36 | private thumbnailsDone: Set<any> |
37 | private actorImagesDone: Set<any> | ||
37 | private historyDone: Set<any> | 38 | private historyDone: Set<any> |
38 | private blacklistDone: Set<any> | 39 | private blacklistDone: Set<any> |
39 | private accountBlocklistDone: Set<any> | 40 | private accountBlocklistDone: Set<any> |
@@ -69,11 +70,21 @@ export class VideoModelBuilder { | |||
69 | for (const row of rows) { | 70 | for (const row of rows) { |
70 | this.buildVideoAndAccount(row) | 71 | this.buildVideoAndAccount(row) |
71 | 72 | ||
72 | const videoModel = this.videosMemo[row.id] | 73 | const videoModel = this.videosMemo[row.id as number] |
73 | 74 | ||
74 | this.setUserHistory(row, videoModel) | 75 | this.setUserHistory(row, videoModel) |
75 | this.addThumbnail(row, videoModel) | 76 | this.addThumbnail(row, videoModel) |
76 | 77 | ||
78 | const channelActor = videoModel.VideoChannel?.Actor | ||
79 | if (channelActor) { | ||
80 | this.addActorAvatar(row, 'VideoChannel.Actor', channelActor) | ||
81 | } | ||
82 | |||
83 | const accountActor = videoModel.VideoChannel?.Account?.Actor | ||
84 | if (accountActor) { | ||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | ||
86 | } | ||
87 | |||
77 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebTorrentFiles) { |
78 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebTorrentFile(row, videoModel) |
79 | } | 90 | } |
@@ -113,6 +124,7 @@ export class VideoModelBuilder { | |||
113 | this.videoFileMemo = {} | 124 | this.videoFileMemo = {} |
114 | 125 | ||
115 | this.thumbnailsDone = new Set() | 126 | this.thumbnailsDone = new Set() |
127 | this.actorImagesDone = new Set() | ||
116 | this.historyDone = new Set() | 128 | this.historyDone = new Set() |
117 | this.blacklistDone = new Set() | 129 | this.blacklistDone = new Set() |
118 | this.liveDone = new Set() | 130 | this.liveDone = new Set() |
@@ -195,13 +207,8 @@ export class VideoModelBuilder { | |||
195 | 207 | ||
196 | private buildActor (row: SQLRow, prefix: string) { | 208 | private buildActor (row: SQLRow, prefix: string) { |
197 | const actorPrefix = `${prefix}.Actor` | 209 | const actorPrefix = `${prefix}.Actor` |
198 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
199 | const serverPrefix = `${actorPrefix}.Server` | 210 | const serverPrefix = `${actorPrefix}.Server` |
200 | 211 | ||
201 | const avatarModel = row[`${avatarPrefix}.id`] !== null | ||
202 | ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts) | ||
203 | : null | ||
204 | |||
205 | const serverModel = row[`${serverPrefix}.id`] !== null | 212 | const serverModel = row[`${serverPrefix}.id`] !== null |
206 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) | 213 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) |
207 | : null | 214 | : null |
@@ -209,8 +216,8 @@ export class VideoModelBuilder { | |||
209 | if (serverModel) serverModel.BlockedBy = [] | 216 | if (serverModel) serverModel.BlockedBy = [] |
210 | 217 | ||
211 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) | 218 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) |
212 | actorModel.Avatar = avatarModel | ||
213 | actorModel.Server = serverModel | 219 | actorModel.Server = serverModel |
220 | actorModel.Avatars = [] | ||
214 | 221 | ||
215 | return actorModel | 222 | return actorModel |
216 | } | 223 | } |
@@ -226,6 +233,18 @@ export class VideoModelBuilder { | |||
226 | this.historyDone.add(id) | 233 | this.historyDone.add(id) |
227 | } | 234 | } |
228 | 235 | ||
236 | private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) { | ||
237 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
238 | const id = row[`${avatarPrefix}.id`] | ||
239 | if (!id || this.actorImagesDone.has(id)) return | ||
240 | |||
241 | const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix) | ||
242 | const avatarModel = new ActorImageModel(attributes, this.buildOpts) | ||
243 | actor.Avatars.push(avatarModel) | ||
244 | |||
245 | this.actorImagesDone.add(id) | ||
246 | } | ||
247 | |||
229 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { | 248 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { |
230 | const id = row['Thumbnails.id'] | 249 | const id = row['Thumbnails.id'] |
231 | if (!id || this.thumbnailsDone.has(id)) return | 250 | if (!id || this.thumbnailsDone.has(id)) return |
diff --git a/server/models/video/sql/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 8a8d2073a..df2ed3fb0 100644 --- a/server/models/video/sql/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -186,8 +186,7 @@ export class VideoTableAttributes { | |||
186 | 'id', | 186 | 'id', |
187 | 'preferredUsername', | 187 | 'preferredUsername', |
188 | 'url', | 188 | 'url', |
189 | 'serverId', | 189 | 'serverId' |
190 | 'avatarId' | ||
191 | ] | 190 | ] |
192 | 191 | ||
193 | if (this.mode === 'get') { | 192 | if (this.mode === 'get') { |
@@ -212,6 +211,7 @@ export class VideoTableAttributes { | |||
212 | getAvatarAttributes () { | 211 | getAvatarAttributes () { |
213 | let attributeKeys = [ | 212 | let attributeKeys = [ |
214 | 'id', | 213 | 'id', |
214 | 'width', | ||
215 | 'filename', | 215 | 'filename', |
216 | 'type', | 216 | 'type', |
217 | 'fileUrl', | 217 | 'fileUrl', |
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index a65c96097..a65c96097 100644 --- a/server/models/video/sql/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 098e15359..098e15359 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index b15b29ec3..b15b29ec3 100644 --- a/server/models/video/sql/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 2c6669bcb..410fd6d3f 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { sendDeleteActor } from '../../lib/activitypub/send' | 32 | import { sendDeleteActor } from '../../lib/activitypub/send' |
33 | import { | 33 | import { |
34 | MChannel, | ||
34 | MChannelActor, | 35 | MChannelActor, |
35 | MChannelAP, | 36 | MChannelAP, |
36 | MChannelBannerAccountDefault, | 37 | MChannelBannerAccountDefault, |
@@ -62,6 +63,7 @@ type AvailableForListOptions = { | |||
62 | search?: string | 63 | search?: string |
63 | host?: string | 64 | host?: string |
64 | handles?: string[] | 65 | handles?: string[] |
66 | forCount?: boolean | ||
65 | } | 67 | } |
66 | 68 | ||
67 | type AvailableWithStatsOptions = { | 69 | type AvailableWithStatsOptions = { |
@@ -116,70 +118,91 @@ export type SummaryOptions = { | |||
116 | }) | 118 | }) |
117 | } | 119 | } |
118 | 120 | ||
119 | let rootWhere: WhereOptions | 121 | if (Array.isArray(options.handles) && options.handles.length !== 0) { |
120 | if (options.handles) { | 122 | const or: string[] = [] |
121 | const or: WhereOptions[] = [] | ||
122 | 123 | ||
123 | for (const handle of options.handles || []) { | 124 | for (const handle of options.handles || []) { |
124 | const [ preferredUsername, host ] = handle.split('@') | 125 | const [ preferredUsername, host ] = handle.split('@') |
125 | 126 | ||
126 | if (!host || host === WEBSERVER.HOST) { | 127 | if (!host || host === WEBSERVER.HOST) { |
127 | or.push({ | 128 | or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`) |
128 | '$Actor.preferredUsername$': preferredUsername, | ||
129 | '$Actor.serverId$': null | ||
130 | }) | ||
131 | } else { | 129 | } else { |
132 | or.push({ | 130 | or.push( |
133 | '$Actor.preferredUsername$': preferredUsername, | 131 | `(` + |
134 | '$Actor.Server.host$': host | 132 | `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` + |
135 | }) | 133 | `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` + |
134 | `)` | ||
135 | ) | ||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | rootWhere = { | 139 | whereActorAnd.push({ |
140 | [Op.or]: or | 140 | id: { |
141 | } | 141 | [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`) |
142 | } | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | const channelInclude: Includeable[] = [] | ||
147 | const accountInclude: Includeable[] = [] | ||
148 | |||
149 | if (options.forCount !== true) { | ||
150 | accountInclude.push({ | ||
151 | model: ServerModel, | ||
152 | required: false | ||
153 | }) | ||
154 | |||
155 | accountInclude.push({ | ||
156 | model: ActorImageModel, | ||
157 | as: 'Avatars', | ||
158 | required: false | ||
159 | }) | ||
160 | |||
161 | channelInclude.push({ | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatars', | ||
164 | required: false | ||
165 | }) | ||
166 | |||
167 | channelInclude.push({ | ||
168 | model: ActorImageModel, | ||
169 | as: 'Banners', | ||
170 | required: false | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | if (options.forCount !== true || serverRequired) { | ||
175 | channelInclude.push({ | ||
176 | model: ServerModel, | ||
177 | duplicating: false, | ||
178 | required: serverRequired, | ||
179 | where: whereServer | ||
180 | }) | ||
142 | } | 181 | } |
143 | 182 | ||
144 | return { | 183 | return { |
145 | where: rootWhere, | ||
146 | include: [ | 184 | include: [ |
147 | { | 185 | { |
148 | attributes: { | 186 | attributes: { |
149 | exclude: unusedActorAttributesForAPI | 187 | exclude: unusedActorAttributesForAPI |
150 | }, | 188 | }, |
151 | model: ActorModel, | 189 | model: ActorModel.unscoped(), |
152 | where: { | 190 | where: { |
153 | [Op.and]: whereActorAnd | 191 | [Op.and]: whereActorAnd |
154 | }, | 192 | }, |
155 | include: [ | 193 | include: channelInclude |
156 | { | ||
157 | model: ServerModel, | ||
158 | required: serverRequired, | ||
159 | where: whereServer | ||
160 | }, | ||
161 | { | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatar', | ||
164 | required: false | ||
165 | }, | ||
166 | { | ||
167 | model: ActorImageModel, | ||
168 | as: 'Banner', | ||
169 | required: false | ||
170 | } | ||
171 | ] | ||
172 | }, | 194 | }, |
173 | { | 195 | { |
174 | model: AccountModel, | 196 | model: AccountModel.unscoped(), |
175 | required: true, | 197 | required: true, |
176 | include: [ | 198 | include: [ |
177 | { | 199 | { |
178 | attributes: { | 200 | attributes: { |
179 | exclude: unusedActorAttributesForAPI | 201 | exclude: unusedActorAttributesForAPI |
180 | }, | 202 | }, |
181 | model: ActorModel, // Default scope includes avatar and server | 203 | model: ActorModel.unscoped(), |
182 | required: true | 204 | required: true, |
205 | include: accountInclude | ||
183 | } | 206 | } |
184 | ] | 207 | ] |
185 | } | 208 | } |
@@ -189,7 +212,7 @@ export type SummaryOptions = { | |||
189 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | 212 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
190 | const include: Includeable[] = [ | 213 | const include: Includeable[] = [ |
191 | { | 214 | { |
192 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 215 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
193 | model: ActorModel.unscoped(), | 216 | model: ActorModel.unscoped(), |
194 | required: options.actorRequired ?? true, | 217 | required: options.actorRequired ?? true, |
195 | include: [ | 218 | include: [ |
@@ -199,8 +222,8 @@ export type SummaryOptions = { | |||
199 | required: false | 222 | required: false |
200 | }, | 223 | }, |
201 | { | 224 | { |
202 | model: ActorImageModel.unscoped(), | 225 | model: ActorImageModel, |
203 | as: 'Avatar', | 226 | as: 'Avatars', |
204 | required: false | 227 | required: false |
205 | } | 228 | } |
206 | ] | 229 | ] |
@@ -245,7 +268,7 @@ export type SummaryOptions = { | |||
245 | { | 268 | { |
246 | model: ActorImageModel, | 269 | model: ActorImageModel, |
247 | required: false, | 270 | required: false, |
248 | as: 'Banner' | 271 | as: 'Banners' |
249 | } | 272 | } |
250 | ] | 273 | ] |
251 | } | 274 | } |
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
474 | order: getSort(parameters.sort) | 497 | order: getSort(parameters.sort) |
475 | } | 498 | } |
476 | 499 | ||
477 | return VideoChannelModel | 500 | const getScope = (forCount: boolean) => { |
478 | .scope({ | 501 | return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] } |
479 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | 502 | } |
480 | }) | 503 | |
481 | .findAndCountAll(query) | 504 | return Promise.all([ |
482 | .then(({ rows, count }) => { | 505 | VideoChannelModel.scope(getScope(true)).count(), |
483 | return { total: count, data: rows } | 506 | VideoChannelModel.scope(getScope(false)).findAll(query) |
484 | }) | 507 | ]).then(([ total, data ]) => ({ total, data })) |
485 | } | 508 | } |
486 | 509 | ||
487 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | 510 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { |
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
519 | where | 542 | where |
520 | } | 543 | } |
521 | 544 | ||
522 | return VideoChannelModel | 545 | const getScope = (forCount: boolean) => { |
523 | .scope({ | 546 | return { |
524 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] | 547 | method: [ |
525 | }) | 548 | ScopeNames.FOR_API, { |
526 | .findAndCountAll(query) | 549 | ...pick(options, [ 'actorId', 'host', 'handles' ]), |
527 | .then(({ rows, count }) => { | 550 | |
528 | return { total: count, data: rows } | 551 | forCount |
529 | }) | 552 | } as AvailableForListOptions |
553 | ] | ||
554 | } | ||
555 | } | ||
556 | |||
557 | return Promise.all([ | ||
558 | VideoChannelModel.scope(getScope(true)).count(query), | ||
559 | VideoChannelModel.scope(getScope(false)).findAll(query) | ||
560 | ]).then(([ total, data ]) => ({ total, data })) | ||
530 | } | 561 | } |
531 | 562 | ||
532 | static listByAccountForAPI (options: { | 563 | static listByAccountForAPI (options: { |
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
552 | } | 583 | } |
553 | : null | 584 | : null |
554 | 585 | ||
555 | const query = { | 586 | const getQuery = (forCount: boolean) => { |
556 | offset: options.start, | 587 | const accountModel = forCount |
557 | limit: options.count, | 588 | ? AccountModel.unscoped() |
558 | order: getSort(options.sort), | 589 | : AccountModel |
559 | include: [ | 590 | |
560 | { | 591 | return { |
561 | model: AccountModel, | 592 | offset: options.start, |
562 | where: { | 593 | limit: options.count, |
563 | id: options.accountId | 594 | order: getSort(options.sort), |
564 | }, | 595 | include: [ |
565 | required: true | 596 | { |
566 | } | 597 | model: accountModel, |
567 | ], | 598 | where: { |
568 | where | 599 | id: options.accountId |
600 | }, | ||
601 | required: true | ||
602 | } | ||
603 | ], | ||
604 | where | ||
605 | } | ||
569 | } | 606 | } |
570 | 607 | ||
571 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] | 608 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
576 | }) | 613 | }) |
577 | } | 614 | } |
578 | 615 | ||
579 | return VideoChannelModel | 616 | return Promise.all([ |
580 | .scope(scopes) | 617 | VideoChannelModel.scope(scopes).count(getQuery(true)), |
581 | .findAndCountAll(query) | 618 | VideoChannelModel.scope(scopes).findAll(getQuery(false)) |
582 | .then(({ rows, count }) => { | 619 | ]).then(([ total, data ]) => ({ total, data })) |
583 | return { total: count, data: rows } | ||
584 | }) | ||
585 | } | 620 | } |
586 | 621 | ||
587 | static listAllByAccount (accountId: number) { | 622 | static listAllByAccount (accountId: number): Promise<MChannel[]> { |
588 | const query = { | 623 | const query = { |
589 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, | 624 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, |
590 | include: [ | 625 | include: [ |
591 | { | 626 | { |
592 | attributes: [], | 627 | attributes: [], |
593 | model: AccountModel, | 628 | model: AccountModel.unscoped(), |
594 | where: { | 629 | where: { |
595 | id: accountId | 630 | id: accountId |
596 | }, | 631 | }, |
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
621 | { | 656 | { |
622 | model: ActorImageModel, | 657 | model: ActorImageModel, |
623 | required: false, | 658 | required: false, |
624 | as: 'Banner' | 659 | as: 'Banners' |
625 | } | 660 | } |
626 | ] | 661 | ] |
627 | } | 662 | } |
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
655 | { | 690 | { |
656 | model: ActorImageModel, | 691 | model: ActorImageModel, |
657 | required: false, | 692 | required: false, |
658 | as: 'Banner' | 693 | as: 'Banners' |
659 | } | 694 | } |
660 | ] | 695 | ] |
661 | } | 696 | } |
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
685 | { | 720 | { |
686 | model: ActorImageModel, | 721 | model: ActorImageModel, |
687 | required: false, | 722 | required: false, |
688 | as: 'Banner' | 723 | as: 'Banners' |
689 | } | 724 | } |
690 | ] | 725 | ] |
691 | } | 726 | } |
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
706 | displayName: this.getDisplayName(), | 741 | displayName: this.getDisplayName(), |
707 | url: actor.url, | 742 | url: actor.url, |
708 | host: actor.host, | 743 | host: actor.host, |
744 | avatars: actor.avatars, | ||
745 | |||
746 | // TODO: remove, deprecated in 4.2 | ||
709 | avatar: actor.avatar | 747 | avatar: actor.avatar |
710 | } | 748 | } |
711 | } | 749 | } |
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
736 | support: this.support, | 774 | support: this.support, |
737 | isLocal: this.Actor.isOwned(), | 775 | isLocal: this.Actor.isOwned(), |
738 | updatedAt: this.updatedAt, | 776 | updatedAt: this.updatedAt, |
777 | |||
739 | ownerAccount: undefined, | 778 | ownerAccount: undefined, |
779 | |||
740 | videosCount, | 780 | videosCount, |
741 | viewsPerDay | 781 | viewsPerDay, |
782 | |||
783 | avatars: actor.avatars, | ||
784 | |||
785 | // TODO: remove, deprecated in 4.2 | ||
786 | avatar: actor.avatar | ||
742 | } | 787 | } |
743 | 788 | ||
744 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 789 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fa77455bc..2d60c6a30 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { uniq } from 'lodash' | 1 | import { uniq } from 'lodash' |
2 | import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -16,8 +16,8 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/typescript-utils' | ||
20 | import { VideoPrivacy } from '@shared/models' | 19 | import { VideoPrivacy } from '@shared/models' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | ||
21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' | 23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
@@ -363,40 +363,43 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | 363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) |
364 | } | 364 | } |
365 | 365 | ||
366 | const query: FindAndCountOptions = { | 366 | const getQuery = (forCount: boolean) => { |
367 | offset: start, | 367 | return { |
368 | limit: count, | 368 | offset: start, |
369 | order: getCommentSort(sort), | 369 | limit: count, |
370 | where, | 370 | order: getCommentSort(sort), |
371 | include: [ | 371 | where, |
372 | { | 372 | include: [ |
373 | model: AccountModel.unscoped(), | 373 | { |
374 | required: true, | 374 | model: AccountModel.unscoped(), |
375 | where: whereAccount, | 375 | required: true, |
376 | include: [ | 376 | where: whereAccount, |
377 | { | 377 | include: [ |
378 | attributes: { | 378 | { |
379 | exclude: unusedActorAttributesForAPI | 379 | attributes: { |
380 | }, | 380 | exclude: unusedActorAttributesForAPI |
381 | model: ActorModel, // Default scope includes avatar and server | 381 | }, |
382 | required: true, | 382 | model: forCount === true |
383 | where: whereActor | 383 | ? ActorModel.unscoped() // Default scope includes avatar and server |
384 | } | 384 | : ActorModel, |
385 | ] | 385 | required: true, |
386 | }, | 386 | where: whereActor |
387 | { | 387 | } |
388 | model: VideoModel.unscoped(), | 388 | ] |
389 | required: true, | 389 | }, |
390 | where: whereVideo | 390 | { |
391 | } | 391 | model: VideoModel.unscoped(), |
392 | ] | 392 | required: true, |
393 | where: whereVideo | ||
394 | } | ||
395 | ] | ||
396 | } | ||
393 | } | 397 | } |
394 | 398 | ||
395 | return VideoCommentModel | 399 | return Promise.all([ |
396 | .findAndCountAll(query) | 400 | VideoCommentModel.count(getQuery(true)), |
397 | .then(({ rows, count }) => { | 401 | VideoCommentModel.findAll(getQuery(false)) |
398 | return { total: count, data: rows } | 402 | ]).then(([ total, data ]) => ({ total, data })) |
399 | }) | ||
400 | } | 403 | } |
401 | 404 | ||
402 | static async listThreadsForApi (parameters: { | 405 | static async listThreadsForApi (parameters: { |
@@ -443,14 +446,20 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
443 | } | 446 | } |
444 | } | 447 | } |
445 | 448 | ||
446 | const scopesList: (string | ScopeOptions)[] = [ | 449 | const findScopesList: (string | ScopeOptions)[] = [ |
447 | ScopeNames.WITH_ACCOUNT_FOR_API, | 450 | ScopeNames.WITH_ACCOUNT_FOR_API, |
448 | { | 451 | { |
449 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 452 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] |
450 | } | 453 | } |
451 | ] | 454 | ] |
452 | 455 | ||
453 | const queryCount = { | 456 | const countScopesList: ScopeOptions[] = [ |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | |||
462 | const notDeletedQueryCount = { | ||
454 | where: { | 463 | where: { |
455 | videoId, | 464 | videoId, |
456 | deletedAt: null, | 465 | deletedAt: null, |
@@ -459,9 +468,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
459 | } | 468 | } |
460 | 469 | ||
461 | return Promise.all([ | 470 | return Promise.all([ |
462 | VideoCommentModel.scope(scopesList).findAndCountAll(queryList), | 471 | VideoCommentModel.scope(findScopesList).findAll(queryList), |
463 | VideoCommentModel.count(queryCount) | 472 | VideoCommentModel.scope(countScopesList).count(queryList), |
464 | ]).then(([ { rows, count }, totalNotDeletedComments ]) => { | 473 | VideoCommentModel.count(notDeletedQueryCount) |
474 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | ||
465 | return { total: count, data: rows, totalNotDeletedComments } | 475 | return { total: count, data: rows, totalNotDeletedComments } |
466 | }) | 476 | }) |
467 | } | 477 | } |
@@ -512,11 +522,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
512 | } | 522 | } |
513 | ] | 523 | ] |
514 | 524 | ||
515 | return VideoCommentModel.scope(scopes) | 525 | return Promise.all([ |
516 | .findAndCountAll(query) | 526 | VideoCommentModel.count(query), |
517 | .then(({ rows, count }) => { | 527 | VideoCommentModel.scope(scopes).findAll(query) |
518 | return { total: count, data: rows } | 528 | ]).then(([ total, data ]) => ({ total, data })) |
519 | }) | ||
520 | } | 529 | } |
521 | 530 | ||
522 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 531 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -565,7 +574,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
565 | transaction: t | 574 | transaction: t |
566 | } | 575 | } |
567 | 576 | ||
568 | return VideoCommentModel.findAndCountAll<MComment>(query) | 577 | return Promise.all([ |
578 | VideoCommentModel.count(query), | ||
579 | VideoCommentModel.findAll<MComment>(query) | ||
580 | ]).then(([ total, data ]) => ({ total, data })) | ||
569 | } | 581 | } |
570 | 582 | ||
571 | static async listForFeed (parameters: { | 583 | static async listForFeed (parameters: { |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 5d2b230e8..1d8296060 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -155,13 +155,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo | |||
155 | where | 155 | where |
156 | } | 156 | } |
157 | 157 | ||
158 | return VideoImportModel.findAndCountAll<MVideoImportDefault>(query) | 158 | return Promise.all([ |
159 | .then(({ rows, count }) => { | 159 | VideoImportModel.unscoped().count(query), |
160 | return { | 160 | VideoImportModel.findAll<MVideoImportDefault>(query) |
161 | data: rows, | 161 | ]).then(([ total, data ]) => ({ total, data })) |
162 | total: count | ||
163 | } | ||
164 | }) | ||
165 | } | 162 | } |
166 | 163 | ||
167 | getTargetIdentifier () { | 164 | getTargetIdentifier () { |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index e20e32f8b..4e4160818 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -23,6 +23,7 @@ import { | |||
23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, | 23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, |
24 | MVideoPlaylistVideoThumbnail | 24 | MVideoPlaylistVideoThumbnail |
25 | } from '@server/types/models/video/video-playlist-element' | 25 | } from '@server/types/models/video/video-playlist-element' |
26 | import { AttributesOnly } from '@shared/typescript-utils' | ||
26 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 27 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
27 | import { VideoPrivacy } from '../../../shared/models/videos' | 28 | import { VideoPrivacy } from '../../../shared/models/videos' |
28 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' | 29 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' |
@@ -32,7 +33,6 @@ import { AccountModel } from '../account/account' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 33 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 34 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
34 | import { VideoPlaylistModel } from './video-playlist' | 35 | import { VideoPlaylistModel } from './video-playlist' |
35 | import { AttributesOnly } from '@shared/typescript-utils' | ||
36 | 36 | ||
37 | @Table({ | 37 | @Table({ |
38 | tableName: 'videoPlaylistElement', | 38 | tableName: 'videoPlaylistElement', |
@@ -208,22 +208,28 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
208 | } | 208 | } |
209 | 209 | ||
210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { | 210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { |
211 | const query = { | 211 | const getQuery = (forCount: boolean) => { |
212 | attributes: [ 'url' ], | 212 | return { |
213 | offset: start, | 213 | attributes: forCount |
214 | limit: count, | 214 | ? [] |
215 | order: getSort('position'), | 215 | : [ 'url' ], |
216 | where: { | 216 | offset: start, |
217 | videoPlaylistId | 217 | limit: count, |
218 | }, | 218 | order: getSort('position'), |
219 | transaction: t | 219 | where: { |
220 | videoPlaylistId | ||
221 | }, | ||
222 | transaction: t | ||
223 | } | ||
220 | } | 224 | } |
221 | 225 | ||
222 | return VideoPlaylistElementModel | 226 | return Promise.all([ |
223 | .findAndCountAll(query) | 227 | VideoPlaylistElementModel.count(getQuery(true)), |
224 | .then(({ rows, count }) => { | 228 | VideoPlaylistElementModel.findAll(getQuery(false)) |
225 | return { total: count, data: rows.map(e => e.url) } | 229 | ]).then(([ total, rows ]) => ({ |
226 | }) | 230 | total, |
231 | data: rows.map(e => e.url) | ||
232 | })) | ||
227 | } | 233 | } |
228 | 234 | ||
229 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { | 235 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index c125db3ff..ae5e237ec 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -86,6 +86,7 @@ type AvailableForListOptions = { | |||
86 | host?: string | 86 | host?: string |
87 | uuids?: string[] | 87 | uuids?: string[] |
88 | withVideos?: boolean | 88 | withVideos?: boolean |
89 | forCount?: boolean | ||
89 | } | 90 | } |
90 | 91 | ||
91 | function getVideoLengthSelect () { | 92 | function getVideoLengthSelect () { |
@@ -239,23 +240,28 @@ function getVideoLengthSelect () { | |||
239 | [Op.and]: whereAnd | 240 | [Op.and]: whereAnd |
240 | } | 241 | } |
241 | 242 | ||
243 | const include: Includeable[] = [ | ||
244 | { | ||
245 | model: AccountModel.scope({ | ||
246 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ] | ||
247 | }), | ||
248 | required: true | ||
249 | } | ||
250 | ] | ||
251 | |||
252 | if (options.forCount !== true) { | ||
253 | include.push({ | ||
254 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
255 | required: false | ||
256 | }) | ||
257 | } | ||
258 | |||
242 | return { | 259 | return { |
243 | attributes: { | 260 | attributes: { |
244 | include: attributesInclude | 261 | include: attributesInclude |
245 | }, | 262 | }, |
246 | where, | 263 | where, |
247 | include: [ | 264 | include |
248 | { | ||
249 | model: AccountModel.scope({ | ||
250 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] | ||
251 | }), | ||
252 | required: true | ||
253 | }, | ||
254 | { | ||
255 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
256 | required: false | ||
257 | } | ||
258 | ] | ||
259 | } as FindOptions | 265 | } as FindOptions |
260 | } | 266 | } |
261 | })) | 267 | })) |
@@ -369,12 +375,23 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
369 | order: getPlaylistSort(options.sort) | 375 | order: getPlaylistSort(options.sort) |
370 | } | 376 | } |
371 | 377 | ||
372 | const scopes: (string | ScopeOptions)[] = [ | 378 | const commonAvailableForListOptions = pick(options, [ |
379 | 'type', | ||
380 | 'followerActorId', | ||
381 | 'accountId', | ||
382 | 'videoChannelId', | ||
383 | 'listMyPlaylists', | ||
384 | 'search', | ||
385 | 'host', | ||
386 | 'uuids' | ||
387 | ]) | ||
388 | |||
389 | const scopesFind: (string | ScopeOptions)[] = [ | ||
373 | { | 390 | { |
374 | method: [ | 391 | method: [ |
375 | ScopeNames.AVAILABLE_FOR_LIST, | 392 | ScopeNames.AVAILABLE_FOR_LIST, |
376 | { | 393 | { |
377 | ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), | 394 | ...commonAvailableForListOptions, |
378 | 395 | ||
379 | withVideos: options.withVideos || false | 396 | withVideos: options.withVideos || false |
380 | } as AvailableForListOptions | 397 | } as AvailableForListOptions |
@@ -384,12 +401,26 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
384 | ScopeNames.WITH_THUMBNAIL | 401 | ScopeNames.WITH_THUMBNAIL |
385 | ] | 402 | ] |
386 | 403 | ||
387 | return VideoPlaylistModel | 404 | const scopesCount: (string | ScopeOptions)[] = [ |
388 | .scope(scopes) | 405 | { |
389 | .findAndCountAll(query) | 406 | method: [ |
390 | .then(({ rows, count }) => { | 407 | ScopeNames.AVAILABLE_FOR_LIST, |
391 | return { total: count, data: rows } | 408 | |
392 | }) | 409 | { |
410 | ...commonAvailableForListOptions, | ||
411 | |||
412 | withVideos: options.withVideos || false, | ||
413 | forCount: true | ||
414 | } as AvailableForListOptions | ||
415 | ] | ||
416 | }, | ||
417 | ScopeNames.WITH_VIDEOS_LENGTH | ||
418 | ] | ||
419 | |||
420 | return Promise.all([ | ||
421 | VideoPlaylistModel.scope(scopesCount).count(), | ||
422 | VideoPlaylistModel.scope(scopesFind).findAll(query) | ||
423 | ]).then(([ count, rows ]) => ({ total: count, data: rows })) | ||
393 | } | 424 | } |
394 | 425 | ||
395 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { | 426 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { |
@@ -419,17 +450,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
419 | Object.assign(where, { videoChannelId: options.channel.id }) | 450 | Object.assign(where, { videoChannelId: options.channel.id }) |
420 | } | 451 | } |
421 | 452 | ||
422 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
423 | attributes: [ 'url' ], | 454 | return { |
424 | offset: start, | 455 | attributes: forCount === true |
425 | limit: count, | 456 | ? [] |
426 | where | 457 | : [ 'url' ], |
458 | offset: start, | ||
459 | limit: count, | ||
460 | where | ||
461 | } | ||
427 | } | 462 | } |
428 | 463 | ||
429 | return VideoPlaylistModel.findAndCountAll(query) | 464 | return Promise.all([ |
430 | .then(({ rows, count }) => { | 465 | VideoPlaylistModel.count(getQuery(true)), |
431 | return { total: count, data: rows.map(p => p.url) } | 466 | VideoPlaylistModel.findAll(getQuery(false)) |
432 | }) | 467 | ]).then(([ total, rows ]) => ({ |
468 | total, | ||
469 | data: rows.map(p => p.url) | ||
470 | })) | ||
433 | } | 471 | } |
434 | 472 | ||
435 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { | 473 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f6659b992..ad95dec6e 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -183,7 +183,10 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode | |||
183 | transaction: t | 183 | transaction: t |
184 | } | 184 | } |
185 | 185 | ||
186 | return VideoShareModel.findAndCountAll(query) | 186 | return Promise.all([ |
187 | VideoShareModel.count(query), | ||
188 | VideoShareModel.findAll(query) | ||
189 | ]).then(([ total, data ]) => ({ total, data })) | ||
187 | } | 190 | } |
188 | 191 | ||
189 | static listRemoteShareUrlsOfLocalVideos () { | 192 | static listRemoteShareUrlsOfLocalVideos () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 9111c71b0..5536334eb 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -114,9 +114,13 @@ import { | |||
114 | videoModelToFormattedJSON | 114 | videoModelToFormattedJSON |
115 | } from './formatter/video-format-utils' | 115 | } from './formatter/video-format-utils' |
116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
117 | import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder' | 117 | import { |
118 | import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | 118 | BuildVideosListQueryOptions, |
119 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | 119 | DisplayOnlyForFollowerOptions, |
120 | VideoModelGetQueryBuilder, | ||
121 | VideosIdListQueryBuilder, | ||
122 | VideosModelListQueryBuilder | ||
123 | } from './sql/video' | ||
120 | import { TagModel } from './tag' | 124 | import { TagModel } from './tag' |
121 | import { ThumbnailModel } from './thumbnail' | 125 | import { ThumbnailModel } from './thumbnail' |
122 | import { VideoBlacklistModel } from './video-blacklist' | 126 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -229,8 +233,8 @@ export type ForAPIOptions = { | |||
229 | required: false | 233 | required: false |
230 | }, | 234 | }, |
231 | { | 235 | { |
232 | model: ActorImageModel.unscoped(), | 236 | model: ActorImageModel, |
233 | as: 'Avatar', | 237 | as: 'Avatars', |
234 | required: false | 238 | required: false |
235 | } | 239 | } |
236 | ] | 240 | ] |
@@ -252,8 +256,8 @@ export type ForAPIOptions = { | |||
252 | required: false | 256 | required: false |
253 | }, | 257 | }, |
254 | { | 258 | { |
255 | model: ActorImageModel.unscoped(), | 259 | model: ActorImageModel, |
256 | as: 'Avatar', | 260 | as: 'Avatars', |
257 | required: false | 261 | required: false |
258 | } | 262 | } |
259 | ] | 263 | ] |