aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>2022-02-28 08:34:43 +0100
committerGitHub <noreply@github.com>2022-02-28 08:34:43 +0100
commitd0800f7661f13fabe7bb6f4aa0ea50764f106405 (patch)
treed43e6b0b6f4a5a32e03487e6464edbcaf288be2a /server/models
parent5cad2ca9db9b9d138f8a33058d10b94a9fd50c69 (diff)
downloadPeerTube-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')
-rw-r--r--server/models/abuse/abuse-message.ts32
-rw-r--r--server/models/account/account-blocklist.ts83
-rw-r--r--server/models/account/account-video-rate.ts56
-rw-r--r--server/models/account/account.ts59
-rw-r--r--server/models/actor/actor-follow.ts263
-rw-r--r--server/models/actor/actor-image.ts67
-rw-r--r--server/models/actor/actor.ts129
-rw-r--r--server/models/server/plugin.ts9
-rw-r--r--server/models/server/server-blocklist.ts13
-rw-r--r--server/models/shared/index.ts1
-rw-r--r--server/models/shared/model-builder.ts101
-rw-r--r--server/models/user/sql/user-notitication-list-query-builder.ts269
-rw-r--r--server/models/user/user-notification.ts275
-rw-r--r--server/models/user/user.ts13
-rw-r--r--server/models/utils.ts2
-rw-r--r--server/models/video/sql/video/index.ts3
-rw-r--r--server/models/video/sql/video/shared/abstract-run-query.ts (renamed from server/models/video/sql/shared/abstract-run-query.ts)0
-rw-r--r--server/models/video/sql/video/shared/abstract-video-query-builder.ts (renamed from server/models/video/sql/shared/abstract-video-query-builder.ts)15
-rw-r--r--server/models/video/sql/video/shared/video-file-query-builder.ts (renamed from server/models/video/sql/shared/video-file-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/shared/video-model-builder.ts (renamed from server/models/video/sql/shared/video-model-builder.ts)51
-rw-r--r--server/models/video/sql/video/shared/video-table-attributes.ts (renamed from server/models/video/sql/shared/video-table-attributes.ts)4
-rw-r--r--server/models/video/sql/video/video-model-get-query-builder.ts (renamed from server/models/video/sql/video-model-get-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts (renamed from server/models/video/sql/videos-id-list-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/videos-model-list-query-builder.ts (renamed from server/models/video/sql/videos-model-list-query-builder.ts)0
-rw-r--r--server/models/video/video-channel.ts209
-rw-r--r--server/models/video/video-comment.ts102
-rw-r--r--server/models/video/video-import.ts11
-rw-r--r--server/models/video/video-playlist-element.ts36
-rw-r--r--server/models/video/video-playlist.ts98
-rw-r--r--server/models/video/video-share.ts5
-rw-r--r--server/models/video/video.ts18
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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' 2import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses'
3import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' 3import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/typescript-utils'
5import { AbuseMessage } from '@shared/models' 4import { AbuseMessage } from '@shared/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' 6import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
7import { getSort, throwIfNotValid } from '../utils' 7import { getSort, throwIfNotValid } from '../utils'
8import { AbuseModel } from './abuse' 8import { AbuseModel } from './abuse'
9import { 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 @@
1import { Op, QueryTypes } from 'sequelize' 1import { FindOptions, Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { handlesToNameAndHost } from '@server/helpers/actors' 3import { handlesToNameAndHost } from '@server/helpers/actors'
4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models'
5import { AttributesOnly } from '@shared/typescript-utils' 5import { AttributesOnly } from '@shared/typescript-utils'
6import { AccountBlock } from '../../../shared/models' 6import { AccountBlock } from '../../../shared/models'
7import { ActorModel } from '../actor/actor' 7import { ActorModel } from '../actor/actor'
@@ -9,27 +9,6 @@ import { ServerModel } from '../server/server'
9import { createSafeIn, getSort, searchAttribute } from '../utils' 9import { createSafeIn, getSort, searchAttribute } from '../utils'
10import { AccountModel } from './account' 10import { AccountModel } from './account'
11 11
12enum 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 @@
1import { difference, values } from 'lodash' 1import { difference, values } from 'lodash'
2import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' 2import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
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'
33import { AttributesOnly } from '@shared/typescript-utils'
34import { ActivityPubActorType } from '@shared/models' 33import { ActivityPubActorType } from '@shared/models'
34import { AttributesOnly } from '@shared/typescript-utils'
35import { FollowState } from '../../../shared/models/actors' 35import { FollowState } from '../../../shared/models/actors'
36import { ActorFollow } from '../../../shared/models/actors/follow.model' 36import { ActorFollow } from '../../../shared/models/actors/follow.model'
37import { logger } from '../../helpers/logger' 37import { logger } from '../../helpers/logger'
38import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' 38import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants'
39import { AccountModel } from '../account/account' 39import { AccountModel } from '../account/account'
40import { ServerModel } from '../server/server' 40import { ServerModel } from '../server/server'
41import { doesExist } from '../shared/query' 41import { 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 @@
1import { remove } from 'fs-extra' 1import { remove } from 'fs-extra'
2import { join } from 'path' 2import { join } from 'path'
3import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import {
4import { 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'
16import { MActorImage, MActorImageFormattable } from '@server/types/models'
17import { getLowercaseExtension } from '@shared/core-utils'
18import { ActivityIconObject, ActorImageType } from '@shared/models'
5import { AttributesOnly } from '@shared/typescript-utils' 19import { AttributesOnly } from '@shared/typescript-utils'
6import { ActorImageType } from '@shared/models'
7import { ActorImage } from '../../../shared/models/actors/actor-image.model' 20import { ActorImage } from '../../../shared/models/actors/actor-image.model'
8import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 21import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
9import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
10import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
11import { LAZY_STATIC_PATHS } from '../../initializers/constants' 24import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants'
12import { throwIfNotValid } from '../utils' 25import { throwIfNotValid } from '../utils'
26import { 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'
19import { getBiggestActorImage } from '@server/lib/actor-image'
19import { ModelCache } from '@server/models/model-cache' 20import { ModelCache } from '@server/models/model-cache'
20import { getLowercaseExtension } from '@shared/core-utils' 21import { getLowercaseExtension } from '@shared/core-utils'
22import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
21import { AttributesOnly } from '@shared/typescript-utils' 23import { AttributesOnly } from '@shared/typescript-utils'
22import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
23import { ActorImage } from '../../../shared/models/actors/actor-image.model'
24import { activityPubContextify } from '../../helpers/activitypub' 24import { activityPubContextify } from '../../helpers/activitypub'
25import { 25import {
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 @@
1import { Op, QueryTypes } from 'sequelize' 1import { Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' 3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/typescript-utils'
5import { ServerBlock } from '@shared/models' 4import { ServerBlock } from '@shared/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
7import { createSafeIn, getSort, searchAttribute } from '../utils' 7import { createSafeIn, getSort, searchAttribute } from '../utils'
8import { ServerModel } from './server' 8import { 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 @@
1export * from './model-builder'
1export * from './query' 2export * from './query'
2export * from './update' 3export * 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 @@
1import { isPlainObject } from 'lodash'
2import { Model as SequelizeModel, Sequelize } from 'sequelize'
3import { logger } from '@server/helpers/logger'
4
5export 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 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { ModelBuilder } from '@server/models/shared'
3import { getSort } from '@server/models/utils'
4import { UserNotificationModelForApi } from '@server/types/models'
5import { ActorImageType } from '@shared/models'
6
7export interface ListNotificationsOptions {
8 userId: number
9 unread?: boolean
10 sort: string
11 offset: number
12 limit: number
13 sequelize: Sequelize
14}
15
16export 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 @@
1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' 1import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { getBiggestActorImage } from '@server/lib/actor-image'
3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' 4import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
4import { uuidToShort } from '@shared/extra-utils' 5import { uuidToShort } from '@shared/extra-utils'
5import { UserNotification, UserNotificationType } from '@shared/models' 6import { UserNotification, UserNotificationType } from '@shared/models'
@@ -7,207 +8,18 @@ import { AttributesOnly } from '@shared/typescript-utils'
7import { isBooleanValid } from '../../helpers/custom-validators/misc' 8import { isBooleanValid } from '../../helpers/custom-validators/misc'
8import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' 9import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
9import { AbuseModel } from '../abuse/abuse' 10import { AbuseModel } from '../abuse/abuse'
10import { VideoAbuseModel } from '../abuse/video-abuse'
11import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
12import { AccountModel } from '../account/account' 11import { AccountModel } from '../account/account'
13import { ActorModel } from '../actor/actor'
14import { ActorFollowModel } from '../actor/actor-follow' 12import { ActorFollowModel } from '../actor/actor-follow'
15import { ActorImageModel } from '../actor/actor-image'
16import { ApplicationModel } from '../application/application' 13import { ApplicationModel } from '../application/application'
17import { PluginModel } from '../server/plugin' 14import { PluginModel } from '../server/plugin'
18import { ServerModel } from '../server/server' 15import { throwIfNotValid } from '../utils'
19import { getSort, throwIfNotValid } from '../utils'
20import { VideoModel } from '../video/video' 16import { VideoModel } from '../video/video'
21import { VideoBlacklistModel } from '../video/video-blacklist' 17import { VideoBlacklistModel } from '../video/video-blacklist'
22import { VideoChannelModel } from '../video/video-channel'
23import { VideoCommentModel } from '../video/video-comment' 18import { VideoCommentModel } from '../video/video-comment'
24import { VideoImportModel } from '../video/video-import' 19import { VideoImportModel } from '../video/video-import'
20import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
25import { UserModel } from './user' 21import { UserModel } from './user'
26 22
27enum ScopeNames {
28 WITH_ALL = 'WITH_ALL'
29}
30
31function 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
52function buildVideoInclude (required: boolean) {
53 return {
54 attributes: [ 'id', 'uuid', 'name' ],
55 model: VideoModel.unscoped(),
56 required
57 }
58}
59
60function 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
69function 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
187function buildWhereIdOrUUID (id: number | string) { 187function 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 @@
1export * from './video-model-get-query-builder'
2export * from './videos-id-list-query-builder'
3export * 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 @@
1import { createSafeIn } from '@server/models/utils' 1import { createSafeIn } from '@server/models/utils'
2import { MUserAccountId } from '@server/types/models' 2import { MUserAccountId } from '@server/types/models'
3import { ActorImageType } from '@shared/models'
3import validator from 'validator' 4import validator from 'validator'
4import { AbstractRunQuery } from './abstract-run-query' 5import { AbstractRunQuery } from './abstract-run-query'
5import { VideoTableAttributes } from './video-table-attributes' 6import { 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'
9import { TrackerModel } from '@server/models/server/tracker' 9import { TrackerModel } from '@server/models/server/tracker'
10import { UserVideoHistoryModel } from '@server/models/user/user-video-history' 10import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
11import { VideoInclude } from '@shared/models' 11import { VideoInclude } from '@shared/models'
12import { ScheduleVideoUpdateModel } from '../../schedule-video-update' 12import { ScheduleVideoUpdateModel } from '../../../schedule-video-update'
13import { TagModel } from '../../tag' 13import { TagModel } from '../../../tag'
14import { ThumbnailModel } from '../../thumbnail' 14import { ThumbnailModel } from '../../../thumbnail'
15import { VideoModel } from '../../video' 15import { VideoModel } from '../../../video'
16import { VideoBlacklistModel } from '../../video-blacklist' 16import { VideoBlacklistModel } from '../../../video-blacklist'
17import { VideoChannelModel } from '../../video-channel' 17import { VideoChannelModel } from '../../../video-channel'
18import { VideoFileModel } from '../../video-file' 18import { VideoFileModel } from '../../../video-file'
19import { VideoLiveModel } from '../../video-live' 19import { VideoLiveModel } from '../../../video-live'
20import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist'
21import { VideoTableAttributes } from './video-table-attributes' 21import { VideoTableAttributes } from './video-table-attributes'
22 22
23type SQLRow = { [id: string]: string | number } 23type 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 {
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { sendDeleteActor } from '../../lib/activitypub/send' 32import { sendDeleteActor } from '../../lib/activitypub/send'
33import { 33import {
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
67type AvailableWithStatsOptions = { 69type 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 @@
1import { uniq } from 'lodash' 1import { uniq } from 'lodash'
2import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 2import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
4 AllowNull, 4 AllowNull,
5 BelongsTo, 5 BelongsTo,
@@ -16,8 +16,8 @@ import {
16} from 'sequelize-typescript' 16} from 'sequelize-typescript'
17import { getServerActor } from '@server/models/application/application' 17import { getServerActor } from '@server/models/application/application'
18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
19import { AttributesOnly } from '@shared/typescript-utils'
20import { VideoPrivacy } from '@shared/models' 19import { VideoPrivacy } from '@shared/models'
20import { AttributesOnly } from '@shared/typescript-utils'
21import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 21import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
22import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 22import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
23import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' 23import { 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'
26import { AttributesOnly } from '@shared/typescript-utils'
26import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 27import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
27import { VideoPrivacy } from '../../../shared/models/videos' 28import { VideoPrivacy } from '../../../shared/models/videos'
28import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' 29import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
@@ -32,7 +33,6 @@ import { AccountModel } from '../account/account'
32import { getSort, throwIfNotValid } from '../utils' 33import { getSort, throwIfNotValid } from '../utils'
33import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' 34import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
34import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
35import { 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 @@
1import { join } from 'path' 1import { join } from 'path'
2import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 2import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
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
91function getVideoLengthSelect () { 92function 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'
116import { ScheduleVideoUpdateModel } from './schedule-video-update' 116import { ScheduleVideoUpdateModel } from './schedule-video-update'
117import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder' 117import {
118import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' 118 BuildVideosListQueryOptions,
119import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' 119 DisplayOnlyForFollowerOptions,
120 VideoModelGetQueryBuilder,
121 VideosIdListQueryBuilder,
122 VideosModelListQueryBuilder
123} from './sql/video'
120import { TagModel } from './tag' 124import { TagModel } from './tag'
121import { ThumbnailModel } from './thumbnail' 125import { ThumbnailModel } from './thumbnail'
122import { VideoBlacklistModel } from './video-blacklist' 126import { 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 ]