diff options
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r-- | server/models/video/video-channel.ts | 169 |
1 files changed, 115 insertions, 54 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 183e7448c..278149d60 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -17,14 +17,13 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { MAccountActor } from '@server/types/models' | 20 | import { MAccountActor } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | 21 | import { AttributesOnly, pick } from '@shared/core-utils' |
23 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 22 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
24 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' | 23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
25 | import { | 24 | import { |
26 | isVideoChannelDescriptionValid, | 25 | isVideoChannelDescriptionValid, |
27 | isVideoChannelNameValid, | 26 | isVideoChannelDisplayNameValid, |
28 | isVideoChannelSupportValid | 27 | isVideoChannelSupportValid |
29 | } from '../../helpers/custom-validators/video-channels' | 28 | } from '../../helpers/custom-validators/video-channels' |
30 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
41 | import { ActorFollowModel } from '../actor/actor-follow' | 40 | import { ActorFollowModel } from '../actor/actor-follow' |
42 | import { ActorImageModel } from '../actor/actor-image' | 41 | import { ActorImageModel } from '../actor/actor-image' |
43 | import { ServerModel } from '../server/server' | 42 | import { ServerModel } from '../server/server' |
43 | import { setAsUpdated } from '../shared' | ||
44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
45 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
46 | import { VideoPlaylistModel } from './video-playlist' | 46 | import { VideoPlaylistModel } from './video-playlist' |
@@ -58,6 +58,8 @@ export enum ScopeNames { | |||
58 | type AvailableForListOptions = { | 58 | type AvailableForListOptions = { |
59 | actorId: number | 59 | actorId: number |
60 | search?: string | 60 | search?: string |
61 | host?: string | ||
62 | handles?: string[] | ||
61 | } | 63 | } |
62 | 64 | ||
63 | type AvailableWithStatsOptions = { | 65 | type AvailableWithStatsOptions = { |
@@ -83,7 +85,62 @@ export type SummaryOptions = { | |||
83 | // Only list local channels OR channels that are on an instance followed by actorId | 85 | // Only list local channels OR channels that are on an instance followed by actorId |
84 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) | 86 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) |
85 | 87 | ||
88 | const whereActorAnd: WhereOptions[] = [ | ||
89 | { | ||
90 | [Op.or]: [ | ||
91 | { | ||
92 | serverId: null | ||
93 | }, | ||
94 | { | ||
95 | serverId: { | ||
96 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | ||
97 | } | ||
98 | } | ||
99 | ] | ||
100 | } | ||
101 | ] | ||
102 | |||
103 | let serverRequired = false | ||
104 | let whereServer: WhereOptions | ||
105 | |||
106 | if (options.host && options.host !== WEBSERVER.HOST) { | ||
107 | serverRequired = true | ||
108 | whereServer = { host: options.host } | ||
109 | } | ||
110 | |||
111 | if (options.host === WEBSERVER.HOST) { | ||
112 | whereActorAnd.push({ | ||
113 | serverId: null | ||
114 | }) | ||
115 | } | ||
116 | |||
117 | let rootWhere: WhereOptions | ||
118 | if (options.handles) { | ||
119 | const or: WhereOptions[] = [] | ||
120 | |||
121 | for (const handle of options.handles || []) { | ||
122 | const [ preferredUsername, host ] = handle.split('@') | ||
123 | |||
124 | if (!host) { | ||
125 | or.push({ | ||
126 | '$Actor.preferredUsername$': preferredUsername, | ||
127 | '$Actor.serverId$': null | ||
128 | }) | ||
129 | } else { | ||
130 | or.push({ | ||
131 | '$Actor.preferredUsername$': preferredUsername, | ||
132 | '$Actor.Server.host$': host | ||
133 | }) | ||
134 | } | ||
135 | } | ||
136 | |||
137 | rootWhere = { | ||
138 | [Op.or]: or | ||
139 | } | ||
140 | } | ||
141 | |||
86 | return { | 142 | return { |
143 | where: rootWhere, | ||
87 | include: [ | 144 | include: [ |
88 | { | 145 | { |
89 | attributes: { | 146 | attributes: { |
@@ -91,19 +148,20 @@ export type SummaryOptions = { | |||
91 | }, | 148 | }, |
92 | model: ActorModel, | 149 | model: ActorModel, |
93 | where: { | 150 | where: { |
94 | [Op.or]: [ | 151 | [Op.and]: whereActorAnd |
95 | { | ||
96 | serverId: null | ||
97 | }, | ||
98 | { | ||
99 | serverId: { | ||
100 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | ||
101 | } | ||
102 | } | ||
103 | ] | ||
104 | }, | 152 | }, |
105 | include: [ | 153 | include: [ |
106 | { | 154 | { |
155 | model: ServerModel, | ||
156 | required: serverRequired, | ||
157 | where: whereServer | ||
158 | }, | ||
159 | { | ||
160 | model: ActorImageModel, | ||
161 | as: 'Avatar', | ||
162 | required: false | ||
163 | }, | ||
164 | { | ||
107 | model: ActorImageModel, | 165 | model: ActorImageModel, |
108 | as: 'Banner', | 166 | as: 'Banner', |
109 | required: false | 167 | required: false |
@@ -250,7 +308,7 @@ export type SummaryOptions = { | |||
250 | export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { | 308 | export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { |
251 | 309 | ||
252 | @AllowNull(false) | 310 | @AllowNull(false) |
253 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) | 311 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name')) |
254 | @Column | 312 | @Column |
255 | name: string | 313 | name: string |
256 | 314 | ||
@@ -380,30 +438,6 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
380 | } | 438 | } |
381 | } | 439 | } |
382 | 440 | ||
383 | static listForApi (parameters: { | ||
384 | actorId: number | ||
385 | start: number | ||
386 | count: number | ||
387 | sort: string | ||
388 | }) { | ||
389 | const { actorId } = parameters | ||
390 | |||
391 | const query = { | ||
392 | offset: parameters.start, | ||
393 | limit: parameters.count, | ||
394 | order: getSort(parameters.sort) | ||
395 | } | ||
396 | |||
397 | return VideoChannelModel | ||
398 | .scope({ | ||
399 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | ||
400 | }) | ||
401 | .findAndCountAll(query) | ||
402 | .then(({ rows, count }) => { | ||
403 | return { total: count, data: rows } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> { | 441 | static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> { |
408 | const query = { | 442 | const query = { |
409 | attributes: [ ], | 443 | attributes: [ ], |
@@ -425,26 +459,43 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
425 | .findAll(query) | 459 | .findAll(query) |
426 | } | 460 | } |
427 | 461 | ||
428 | static searchForApi (options: { | 462 | static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & { |
429 | actorId: number | ||
430 | search: string | ||
431 | start: number | 463 | start: number |
432 | count: number | 464 | count: number |
433 | sort: string | 465 | sort: string |
434 | }) { | 466 | }) { |
435 | const attributesInclude = [] | 467 | const { actorId } = parameters |
436 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | ||
437 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | ||
438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | ||
439 | 468 | ||
440 | const query = { | 469 | const query = { |
441 | attributes: { | 470 | offset: parameters.start, |
442 | include: attributesInclude | 471 | limit: parameters.count, |
443 | }, | 472 | order: getSort(parameters.sort) |
444 | offset: options.start, | 473 | } |
445 | limit: options.count, | 474 | |
446 | order: getSort(options.sort), | 475 | return VideoChannelModel |
447 | where: { | 476 | .scope({ |
477 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | ||
478 | }) | ||
479 | .findAndCountAll(query) | ||
480 | .then(({ rows, count }) => { | ||
481 | return { total: count, data: rows } | ||
482 | }) | ||
483 | } | ||
484 | |||
485 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | ||
486 | start: number | ||
487 | count: number | ||
488 | sort: string | ||
489 | }) { | ||
490 | let attributesInclude: any[] = [ literal('0 as similarity') ] | ||
491 | let where: WhereOptions | ||
492 | |||
493 | if (options.search) { | ||
494 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | ||
495 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | ||
496 | attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ] | ||
497 | |||
498 | where = { | ||
448 | [Op.or]: [ | 499 | [Op.or]: [ |
449 | Sequelize.literal( | 500 | Sequelize.literal( |
450 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | 501 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
@@ -456,9 +507,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
456 | } | 507 | } |
457 | } | 508 | } |
458 | 509 | ||
510 | const query = { | ||
511 | attributes: { | ||
512 | include: attributesInclude | ||
513 | }, | ||
514 | offset: options.start, | ||
515 | limit: options.count, | ||
516 | order: getSort(options.sort), | ||
517 | where | ||
518 | } | ||
519 | |||
459 | return VideoChannelModel | 520 | return VideoChannelModel |
460 | .scope({ | 521 | .scope({ |
461 | method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] | 522 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] |
462 | }) | 523 | }) |
463 | .findAndCountAll(query) | 524 | .findAndCountAll(query) |
464 | .then(({ rows, count }) => { | 525 | .then(({ rows, count }) => { |