X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-channel.ts;h=2c6669bcba9262ef9700502b0b186c47f597b0f3;hb=57a9b61a4aeead74e8800bbfad82ef433313b204;hp=8e4b78723cf3012c3b92f035fe968c3114fe9b0a;hpb=7d9ba5c08999c6482f0bc5e0c09c6f55b7724090;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 8e4b78723..2c6669bcb 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -1,4 +1,4 @@ -import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' +import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize' import { AllowNull, BeforeDestroy, @@ -17,13 +17,15 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { setAsUpdated } from '@server/helpers/database-utils' +import { CONFIG } from '@server/initializers/config' import { MAccountActor } from '@server/types/models' +import { pick } from '@shared/core-utils' +import { AttributesOnly } from '@shared/typescript-utils' import { ActivityPubActor } from '../../../shared/models/activitypub' import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' import { isVideoChannelDescriptionValid, - isVideoChannelNameValid, + isVideoChannelDisplayNameValid, isVideoChannelSupportValid } from '../../helpers/custom-validators/video-channels' import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' @@ -40,6 +42,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' import { ActorFollowModel } from '../actor/actor-follow' import { ActorImageModel } from '../actor/actor-image' import { ServerModel } from '../server/server' +import { setAsUpdated } from '../shared' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' import { VideoPlaylistModel } from './video-playlist' @@ -57,6 +60,8 @@ export enum ScopeNames { type AvailableForListOptions = { actorId: number search?: string + host?: string + handles?: string[] } type AvailableWithStatsOptions = { @@ -82,7 +87,62 @@ export type SummaryOptions = { // Only list local channels OR channels that are on an instance followed by actorId const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) + const whereActorAnd: WhereOptions[] = [ + { + [Op.or]: [ + { + serverId: null + }, + { + serverId: { + [Op.in]: Sequelize.literal(inQueryInstanceFollow) + } + } + ] + } + ] + + let serverRequired = false + let whereServer: WhereOptions + + if (options.host && options.host !== WEBSERVER.HOST) { + serverRequired = true + whereServer = { host: options.host } + } + + if (options.host === WEBSERVER.HOST) { + whereActorAnd.push({ + serverId: null + }) + } + + let rootWhere: WhereOptions + if (options.handles) { + const or: WhereOptions[] = [] + + for (const handle of options.handles || []) { + const [ preferredUsername, host ] = handle.split('@') + + if (!host || host === WEBSERVER.HOST) { + or.push({ + '$Actor.preferredUsername$': preferredUsername, + '$Actor.serverId$': null + }) + } else { + or.push({ + '$Actor.preferredUsername$': preferredUsername, + '$Actor.Server.host$': host + }) + } + } + + rootWhere = { + [Op.or]: or + } + } + return { + where: rootWhere, include: [ { attributes: { @@ -90,18 +150,19 @@ export type SummaryOptions = { }, model: ActorModel, where: { - [Op.or]: [ - { - serverId: null - }, - { - serverId: { - [Op.in]: Sequelize.literal(inQueryInstanceFollow) - } - } - ] + [Op.and]: whereActorAnd }, include: [ + { + model: ServerModel, + required: serverRequired, + where: whereServer + }, + { + model: ActorImageModel, + as: 'Avatar', + required: false + }, { model: ActorImageModel, as: 'Banner', @@ -246,10 +307,10 @@ export type SummaryOptions = { } ] }) -export class VideoChannelModel extends Model { +export class VideoChannelModel extends Model>> { @AllowNull(false) - @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) + @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name')) @Column name: string @@ -290,8 +351,7 @@ export class VideoChannelModel extends Model { @BelongsTo(() => AccountModel, { foreignKey: { allowNull: false - }, - hooks: true + } }) Account: AccountModel @@ -380,30 +440,6 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` } } - static listForApi (parameters: { - actorId: number - start: number - count: number - sort: string - }) { - const { actorId } = parameters - - const query = { - offset: parameters.start, - limit: parameters.count, - order: getSort(parameters.sort) - } - - return VideoChannelModel - .scope({ - method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] - }) - .findAndCountAll(query) - .then(({ rows, count }) => { - return { total: count, data: rows } - }) - } - static listLocalsForSitemap (sort: string): Promise { const query = { attributes: [ ], @@ -425,26 +461,43 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` .findAll(query) } - static searchForApi (options: { - actorId: number - search: string + static listForApi (parameters: Pick & { start: number count: number sort: string }) { - const attributesInclude = [] - const escapedSearch = VideoModel.sequelize.escape(options.search) - const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') - attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) + const { actorId } = parameters const query = { - attributes: { - include: attributesInclude - }, - offset: options.start, - limit: options.count, - order: getSort(options.sort), - where: { + offset: parameters.start, + limit: parameters.count, + order: getSort(parameters.sort) + } + + return VideoChannelModel + .scope({ + method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] + }) + .findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) + } + + static searchForApi (options: Pick & { + start: number + count: number + sort: string + }) { + let attributesInclude: any[] = [ literal('0 as similarity') ] + let where: WhereOptions + + if (options.search) { + const escapedSearch = VideoChannelModel.sequelize.escape(options.search) + const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') + attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ] + + where = { [Op.or]: [ Sequelize.literal( 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' @@ -456,9 +509,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` } } + const query = { + attributes: { + include: attributesInclude + }, + offset: options.start, + limit: options.count, + order: getSort(options.sort), + where + } + return VideoChannelModel .scope({ - method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] + method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] }) .findAndCountAll(query) .then(({ rows, count }) => { @@ -466,7 +529,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` }) } - static listByAccount (options: { + static listByAccountForAPI (options: { accountId: number start: number count: number @@ -521,10 +584,28 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` }) } - static loadAndPopulateAccount (id: number): Promise { + static listAllByAccount (accountId: number) { + const query = { + limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, + include: [ + { + attributes: [], + model: AccountModel, + where: { + id: accountId + }, + required: true + } + ] + } + + return VideoChannelModel.findAll(query) + } + + static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise { return VideoChannelModel.unscoped() .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) - .findByPk(id) + .findByPk(id, { transaction }) } static loadByUrlAndPopulateAccount (url: string): Promise { @@ -692,7 +773,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` return this.Actor.isOutdated() } - setAsUpdated (transaction: Transaction) { + setAsUpdated (transaction?: Transaction) { return setAsUpdated('videoChannel', this.id, transaction) } }