diff options
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r-- | server/models/video/video-channel.ts | 209 |
1 files changed, 127 insertions, 82 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 2c6669bcb..410fd6d3f 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { sendDeleteActor } from '../../lib/activitypub/send' | 32 | import { sendDeleteActor } from '../../lib/activitypub/send' |
33 | import { | 33 | import { |
34 | MChannel, | ||
34 | MChannelActor, | 35 | MChannelActor, |
35 | MChannelAP, | 36 | MChannelAP, |
36 | MChannelBannerAccountDefault, | 37 | MChannelBannerAccountDefault, |
@@ -62,6 +63,7 @@ type AvailableForListOptions = { | |||
62 | search?: string | 63 | search?: string |
63 | host?: string | 64 | host?: string |
64 | handles?: string[] | 65 | handles?: string[] |
66 | forCount?: boolean | ||
65 | } | 67 | } |
66 | 68 | ||
67 | type AvailableWithStatsOptions = { | 69 | type AvailableWithStatsOptions = { |
@@ -116,70 +118,91 @@ export type SummaryOptions = { | |||
116 | }) | 118 | }) |
117 | } | 119 | } |
118 | 120 | ||
119 | let rootWhere: WhereOptions | 121 | if (Array.isArray(options.handles) && options.handles.length !== 0) { |
120 | if (options.handles) { | 122 | const or: string[] = [] |
121 | const or: WhereOptions[] = [] | ||
122 | 123 | ||
123 | for (const handle of options.handles || []) { | 124 | for (const handle of options.handles || []) { |
124 | const [ preferredUsername, host ] = handle.split('@') | 125 | const [ preferredUsername, host ] = handle.split('@') |
125 | 126 | ||
126 | if (!host || host === WEBSERVER.HOST) { | 127 | if (!host || host === WEBSERVER.HOST) { |
127 | or.push({ | 128 | or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`) |
128 | '$Actor.preferredUsername$': preferredUsername, | ||
129 | '$Actor.serverId$': null | ||
130 | }) | ||
131 | } else { | 129 | } else { |
132 | or.push({ | 130 | or.push( |
133 | '$Actor.preferredUsername$': preferredUsername, | 131 | `(` + |
134 | '$Actor.Server.host$': host | 132 | `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` + |
135 | }) | 133 | `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` + |
134 | `)` | ||
135 | ) | ||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | rootWhere = { | 139 | whereActorAnd.push({ |
140 | [Op.or]: or | 140 | id: { |
141 | } | 141 | [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`) |
142 | } | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | const channelInclude: Includeable[] = [] | ||
147 | const accountInclude: Includeable[] = [] | ||
148 | |||
149 | if (options.forCount !== true) { | ||
150 | accountInclude.push({ | ||
151 | model: ServerModel, | ||
152 | required: false | ||
153 | }) | ||
154 | |||
155 | accountInclude.push({ | ||
156 | model: ActorImageModel, | ||
157 | as: 'Avatars', | ||
158 | required: false | ||
159 | }) | ||
160 | |||
161 | channelInclude.push({ | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatars', | ||
164 | required: false | ||
165 | }) | ||
166 | |||
167 | channelInclude.push({ | ||
168 | model: ActorImageModel, | ||
169 | as: 'Banners', | ||
170 | required: false | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | if (options.forCount !== true || serverRequired) { | ||
175 | channelInclude.push({ | ||
176 | model: ServerModel, | ||
177 | duplicating: false, | ||
178 | required: serverRequired, | ||
179 | where: whereServer | ||
180 | }) | ||
142 | } | 181 | } |
143 | 182 | ||
144 | return { | 183 | return { |
145 | where: rootWhere, | ||
146 | include: [ | 184 | include: [ |
147 | { | 185 | { |
148 | attributes: { | 186 | attributes: { |
149 | exclude: unusedActorAttributesForAPI | 187 | exclude: unusedActorAttributesForAPI |
150 | }, | 188 | }, |
151 | model: ActorModel, | 189 | model: ActorModel.unscoped(), |
152 | where: { | 190 | where: { |
153 | [Op.and]: whereActorAnd | 191 | [Op.and]: whereActorAnd |
154 | }, | 192 | }, |
155 | include: [ | 193 | include: channelInclude |
156 | { | ||
157 | model: ServerModel, | ||
158 | required: serverRequired, | ||
159 | where: whereServer | ||
160 | }, | ||
161 | { | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatar', | ||
164 | required: false | ||
165 | }, | ||
166 | { | ||
167 | model: ActorImageModel, | ||
168 | as: 'Banner', | ||
169 | required: false | ||
170 | } | ||
171 | ] | ||
172 | }, | 194 | }, |
173 | { | 195 | { |
174 | model: AccountModel, | 196 | model: AccountModel.unscoped(), |
175 | required: true, | 197 | required: true, |
176 | include: [ | 198 | include: [ |
177 | { | 199 | { |
178 | attributes: { | 200 | attributes: { |
179 | exclude: unusedActorAttributesForAPI | 201 | exclude: unusedActorAttributesForAPI |
180 | }, | 202 | }, |
181 | model: ActorModel, // Default scope includes avatar and server | 203 | model: ActorModel.unscoped(), |
182 | required: true | 204 | required: true, |
205 | include: accountInclude | ||
183 | } | 206 | } |
184 | ] | 207 | ] |
185 | } | 208 | } |
@@ -189,7 +212,7 @@ export type SummaryOptions = { | |||
189 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | 212 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
190 | const include: Includeable[] = [ | 213 | const include: Includeable[] = [ |
191 | { | 214 | { |
192 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 215 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
193 | model: ActorModel.unscoped(), | 216 | model: ActorModel.unscoped(), |
194 | required: options.actorRequired ?? true, | 217 | required: options.actorRequired ?? true, |
195 | include: [ | 218 | include: [ |
@@ -199,8 +222,8 @@ export type SummaryOptions = { | |||
199 | required: false | 222 | required: false |
200 | }, | 223 | }, |
201 | { | 224 | { |
202 | model: ActorImageModel.unscoped(), | 225 | model: ActorImageModel, |
203 | as: 'Avatar', | 226 | as: 'Avatars', |
204 | required: false | 227 | required: false |
205 | } | 228 | } |
206 | ] | 229 | ] |
@@ -245,7 +268,7 @@ export type SummaryOptions = { | |||
245 | { | 268 | { |
246 | model: ActorImageModel, | 269 | model: ActorImageModel, |
247 | required: false, | 270 | required: false, |
248 | as: 'Banner' | 271 | as: 'Banners' |
249 | } | 272 | } |
250 | ] | 273 | ] |
251 | } | 274 | } |
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
474 | order: getSort(parameters.sort) | 497 | order: getSort(parameters.sort) |
475 | } | 498 | } |
476 | 499 | ||
477 | return VideoChannelModel | 500 | const getScope = (forCount: boolean) => { |
478 | .scope({ | 501 | return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] } |
479 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | 502 | } |
480 | }) | 503 | |
481 | .findAndCountAll(query) | 504 | return Promise.all([ |
482 | .then(({ rows, count }) => { | 505 | VideoChannelModel.scope(getScope(true)).count(), |
483 | return { total: count, data: rows } | 506 | VideoChannelModel.scope(getScope(false)).findAll(query) |
484 | }) | 507 | ]).then(([ total, data ]) => ({ total, data })) |
485 | } | 508 | } |
486 | 509 | ||
487 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | 510 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { |
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
519 | where | 542 | where |
520 | } | 543 | } |
521 | 544 | ||
522 | return VideoChannelModel | 545 | const getScope = (forCount: boolean) => { |
523 | .scope({ | 546 | return { |
524 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] | 547 | method: [ |
525 | }) | 548 | ScopeNames.FOR_API, { |
526 | .findAndCountAll(query) | 549 | ...pick(options, [ 'actorId', 'host', 'handles' ]), |
527 | .then(({ rows, count }) => { | 550 | |
528 | return { total: count, data: rows } | 551 | forCount |
529 | }) | 552 | } as AvailableForListOptions |
553 | ] | ||
554 | } | ||
555 | } | ||
556 | |||
557 | return Promise.all([ | ||
558 | VideoChannelModel.scope(getScope(true)).count(query), | ||
559 | VideoChannelModel.scope(getScope(false)).findAll(query) | ||
560 | ]).then(([ total, data ]) => ({ total, data })) | ||
530 | } | 561 | } |
531 | 562 | ||
532 | static listByAccountForAPI (options: { | 563 | static listByAccountForAPI (options: { |
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
552 | } | 583 | } |
553 | : null | 584 | : null |
554 | 585 | ||
555 | const query = { | 586 | const getQuery = (forCount: boolean) => { |
556 | offset: options.start, | 587 | const accountModel = forCount |
557 | limit: options.count, | 588 | ? AccountModel.unscoped() |
558 | order: getSort(options.sort), | 589 | : AccountModel |
559 | include: [ | 590 | |
560 | { | 591 | return { |
561 | model: AccountModel, | 592 | offset: options.start, |
562 | where: { | 593 | limit: options.count, |
563 | id: options.accountId | 594 | order: getSort(options.sort), |
564 | }, | 595 | include: [ |
565 | required: true | 596 | { |
566 | } | 597 | model: accountModel, |
567 | ], | 598 | where: { |
568 | where | 599 | id: options.accountId |
600 | }, | ||
601 | required: true | ||
602 | } | ||
603 | ], | ||
604 | where | ||
605 | } | ||
569 | } | 606 | } |
570 | 607 | ||
571 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] | 608 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
576 | }) | 613 | }) |
577 | } | 614 | } |
578 | 615 | ||
579 | return VideoChannelModel | 616 | return Promise.all([ |
580 | .scope(scopes) | 617 | VideoChannelModel.scope(scopes).count(getQuery(true)), |
581 | .findAndCountAll(query) | 618 | VideoChannelModel.scope(scopes).findAll(getQuery(false)) |
582 | .then(({ rows, count }) => { | 619 | ]).then(([ total, data ]) => ({ total, data })) |
583 | return { total: count, data: rows } | ||
584 | }) | ||
585 | } | 620 | } |
586 | 621 | ||
587 | static listAllByAccount (accountId: number) { | 622 | static listAllByAccount (accountId: number): Promise<MChannel[]> { |
588 | const query = { | 623 | const query = { |
589 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, | 624 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, |
590 | include: [ | 625 | include: [ |
591 | { | 626 | { |
592 | attributes: [], | 627 | attributes: [], |
593 | model: AccountModel, | 628 | model: AccountModel.unscoped(), |
594 | where: { | 629 | where: { |
595 | id: accountId | 630 | id: accountId |
596 | }, | 631 | }, |
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
621 | { | 656 | { |
622 | model: ActorImageModel, | 657 | model: ActorImageModel, |
623 | required: false, | 658 | required: false, |
624 | as: 'Banner' | 659 | as: 'Banners' |
625 | } | 660 | } |
626 | ] | 661 | ] |
627 | } | 662 | } |
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
655 | { | 690 | { |
656 | model: ActorImageModel, | 691 | model: ActorImageModel, |
657 | required: false, | 692 | required: false, |
658 | as: 'Banner' | 693 | as: 'Banners' |
659 | } | 694 | } |
660 | ] | 695 | ] |
661 | } | 696 | } |
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
685 | { | 720 | { |
686 | model: ActorImageModel, | 721 | model: ActorImageModel, |
687 | required: false, | 722 | required: false, |
688 | as: 'Banner' | 723 | as: 'Banners' |
689 | } | 724 | } |
690 | ] | 725 | ] |
691 | } | 726 | } |
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
706 | displayName: this.getDisplayName(), | 741 | displayName: this.getDisplayName(), |
707 | url: actor.url, | 742 | url: actor.url, |
708 | host: actor.host, | 743 | host: actor.host, |
744 | avatars: actor.avatars, | ||
745 | |||
746 | // TODO: remove, deprecated in 4.2 | ||
709 | avatar: actor.avatar | 747 | avatar: actor.avatar |
710 | } | 748 | } |
711 | } | 749 | } |
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
736 | support: this.support, | 774 | support: this.support, |
737 | isLocal: this.Actor.isOwned(), | 775 | isLocal: this.Actor.isOwned(), |
738 | updatedAt: this.updatedAt, | 776 | updatedAt: this.updatedAt, |
777 | |||
739 | ownerAccount: undefined, | 778 | ownerAccount: undefined, |
779 | |||
740 | videosCount, | 780 | videosCount, |
741 | viewsPerDay | 781 | viewsPerDay, |
782 | |||
783 | avatars: actor.avatars, | ||
784 | |||
785 | // TODO: remove, deprecated in 4.2 | ||
786 | avatar: actor.avatar | ||
742 | } | 787 | } |
743 | 788 | ||
744 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 789 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |