aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video-channel.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r--server/models/video/video-channel.ts169
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 @@
1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
2import { 2import {
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'
20import { setAsUpdated } from '@server/helpers/database-utils'
21import { MAccountActor } from '@server/types/models' 20import { MAccountActor } from '@server/types/models'
22import { AttributesOnly } from '@shared/core-utils' 21import { AttributesOnly, pick } from '@shared/core-utils'
23import { ActivityPubActor } from '../../../shared/models/activitypub' 22import { ActivityPubActor } from '../../../shared/models/activitypub'
24import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' 23import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
25import { 24import {
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'
30import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
41import { ActorFollowModel } from '../actor/actor-follow' 40import { ActorFollowModel } from '../actor/actor-follow'
42import { ActorImageModel } from '../actor/actor-image' 41import { ActorImageModel } from '../actor/actor-image'
43import { ServerModel } from '../server/server' 42import { ServerModel } from '../server/server'
43import { setAsUpdated } from '../shared'
44import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 44import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
45import { VideoModel } from './video' 45import { VideoModel } from './video'
46import { VideoPlaylistModel } from './video-playlist' 46import { VideoPlaylistModel } from './video-playlist'
@@ -58,6 +58,8 @@ export enum ScopeNames {
58type AvailableForListOptions = { 58type AvailableForListOptions = {
59 actorId: number 59 actorId: number
60 search?: string 60 search?: string
61 host?: string
62 handles?: string[]
61} 63}
62 64
63type AvailableWithStatsOptions = { 65type 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 = {
250export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { 308export 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 }) => {