diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts index c8f0a0a0b..be0b6b9a2 100644 --- a/server/controllers/api/search/search-video-channels.ts +++ b/server/controllers/api/search/search-video-channels.ts @@ -98,7 +98,8 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr search: query.search, start: query.start, count: query.count, - sort: query.sort + sort: query.sort, + host: query.host }, 'filter:api.search.video-channels.local.list.params') const resultList = await Hooks.wrapPromiseFun( diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts index f55b1fba3..60d1a44f7 100644 --- a/server/controllers/api/search/search-video-playlists.ts +++ b/server/controllers/api/search/search-video-playlists.ts @@ -88,7 +88,8 @@ async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: ex search: query.search, start: query.start, count: query.count, - sort: query.sort + sort: query.sort, + host: query.host }, 'filter:api.search.video-playlists.local.list.params') const resultList = await Hooks.wrapPromiseFun( diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index 422774022..ec35295df 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts @@ -1,6 +1,6 @@ import * as retry from 'async/retry' import * as Bluebird from 'bluebird' -import { BindOrReplacements, QueryTypes, Transaction } from 'sequelize' +import { Transaction } from 'sequelize' import { Model } from 'sequelize-typescript' import { sequelizeTypescript } from '@server/initializers/database' import { logger } from './logger' @@ -95,18 +95,6 @@ function deleteAllModels > (models: T[], transa return Promise.all(models.map(f => f.destroy({ transaction }))) } -// Sequelize always skip the update if we only update updatedAt field -function setAsUpdated (table: string, id: number, transaction?: Transaction) { - return sequelizeTypescript.query( - `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, - { - replacements: { table, id, updatedAt: new Date() }, - type: QueryTypes.UPDATE, - transaction - } - ) -} - // --------------------------------------------------------------------------- function runInReadCommittedTransaction (fn: (t: Transaction) => Promise) { @@ -123,19 +111,6 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) { // --------------------------------------------------------------------------- -function doesExist (query: string, bind?: BindOrReplacements) { - const options = { - type: QueryTypes.SELECT as QueryTypes.SELECT, - bind, - raw: true - } - - return sequelizeTypescript.query(query, options) - .then(results => results.length === 1) -} - -// --------------------------------------------------------------------------- - export { resetSequelizeInstance, retryTransactionWrapper, @@ -144,7 +119,5 @@ export { afterCommitIfTransaction, filterNonExistingModels, deleteAllModels, - setAsUpdated, - runInReadCommittedTransaction, - doesExist + runInReadCommittedTransaction } diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 6bb335127..ea6a490b2 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts @@ -43,7 +43,14 @@ const videosSearchValidator = [ const videoChannelsListSearchValidator = [ query('search').not().isEmpty().withMessage('Should have a valid search'), - query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), + + query('host') + .optional() + .custom(isHostValid).withMessage('Should have a valid host'), + + query('searchTarget') + .optional() + .custom(isSearchTargetValid).withMessage('Should have a valid search target'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking video channels search query', { parameters: req.query }) @@ -56,7 +63,14 @@ const videoChannelsListSearchValidator = [ const videoPlaylistsListSearchValidator = [ query('search').not().isEmpty().withMessage('Should have a valid search'), - query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), + + query('host') + .optional() + .custom(isHostValid).withMessage('Should have a valid host'), + + query('searchTarget') + .optional() + .custom(isSearchTargetValid).withMessage('Should have a valid search target'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking video playlists search query', { parameters: req.query }) diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 665ecd595..37194a119 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -52,6 +52,7 @@ export enum ScopeNames { export type SummaryOptions = { actorRequired?: boolean // Default: true whereActor?: WhereOptions + whereServer?: WhereOptions withAccountBlockerIds?: number[] } @@ -65,12 +66,11 @@ export type SummaryOptions = { })) @Scopes(() => ({ [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { - const whereActor = options.whereActor || undefined - const serverInclude: IncludeOptions = { attributes: [ 'host' ], model: ServerModel.unscoped(), - required: false + required: !!options.whereServer, + where: options.whereServer } const queryInclude: Includeable[] = [ @@ -78,7 +78,7 @@ export type SummaryOptions = { attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], model: ActorModel.unscoped(), required: options.actorRequired ?? true, - where: whereActor, + where: options.whereActor, include: [ serverInclude, diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 3080e02a6..283856d3f 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts @@ -19,7 +19,6 @@ import { UpdatedAt } from 'sequelize-typescript' import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' -import { doesExist } from '@server/helpers/database-utils' import { getServerActor } from '@server/models/application/application' import { MActorFollowActorsDefault, @@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger' import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' import { AccountModel } from '../account/account' import { ServerModel } from '../server/server' +import { doesExist } from '../shared/query' import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' import { ActorModel, unusedActorAttributesForAPI } from './actor' diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts new file mode 100644 index 000000000..5b97510e0 --- /dev/null +++ b/server/models/shared/index.ts @@ -0,0 +1,2 @@ +export * from './query' +export * from './update' diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts new file mode 100644 index 000000000..036cc13c6 --- /dev/null +++ b/server/models/shared/query.ts @@ -0,0 +1,17 @@ +import { BindOrReplacements, QueryTypes } from 'sequelize' +import { sequelizeTypescript } from '@server/initializers/database' + +function doesExist (query: string, bind?: BindOrReplacements) { + const options = { + type: QueryTypes.SELECT as QueryTypes.SELECT, + bind, + raw: true + } + + return sequelizeTypescript.query(query, options) + .then(results => results.length === 1) +} + +export { + doesExist +} diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts new file mode 100644 index 000000000..d338211e3 --- /dev/null +++ b/server/models/shared/update.ts @@ -0,0 +1,18 @@ +import { QueryTypes, Transaction } from 'sequelize' +import { sequelizeTypescript } from '@server/initializers/database' + +// Sequelize always skip the update if we only update updatedAt field +function setAsUpdated (table: string, id: number, transaction?: Transaction) { + return sequelizeTypescript.query( + `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, + { + replacements: { table, id, updatedAt: new Date() }, + type: QueryTypes.UPDATE, + transaction + } + ) +} + +export { + setAsUpdated +} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 183e7448c..9aa271711 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,7 +17,6 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { setAsUpdated } from '@server/helpers/database-utils' import { MAccountActor } from '@server/types/models' import { AttributesOnly } from '@shared/core-utils' import { ActivityPubActor } from '../../../shared/models/activitypub' @@ -41,6 +40,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' @@ -58,6 +58,7 @@ export enum ScopeNames { type AvailableForListOptions = { actorId: number search?: string + host?: string } type AvailableWithStatsOptions = { @@ -83,6 +84,33 @@ export type SummaryOptions = { // Only list local channels OR channels that are on an instance followed by actorId const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) + const whereActor = { + [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) { + Object.assign(whereActor, { + [Op.and]: [ { serverId: null } ] + }) + } + return { include: [ { @@ -90,19 +118,18 @@ export type SummaryOptions = { exclude: unusedActorAttributesForAPI }, model: ActorModel, - where: { - [Op.or]: [ - { - serverId: null - }, - { - serverId: { - [Op.in]: Sequelize.literal(inQueryInstanceFollow) - } - } - ] - }, + where: whereActor, include: [ + { + model: ServerModel, + required: serverRequired, + where: whereServer + }, + { + model: ActorImageModel, + as: 'Avatar', + required: false + }, { model: ActorImageModel, as: 'Banner', @@ -431,6 +458,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` start: number count: number sort: string + + host?: string }) { const attributesInclude = [] const escapedSearch = VideoChannelModel.sequelize.escape(options.search) @@ -458,7 +487,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` return VideoChannelModel .scope({ - method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] + method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host } as AvailableForListOptions ] }) .findAndCountAll(query) .then(({ rows, count }) => { diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 797a85a4e..09fc5288b 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -21,7 +21,6 @@ import { import { Where } from 'sequelize/types/lib/utils' import validator from 'validator' import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' -import { doesExist } from '@server/helpers/database-utils' import { logger } from '@server/helpers/logger' import { extractVideo } from '@server/helpers/video' import { getTorrentFilePath } from '@server/lib/video-paths' @@ -45,6 +44,7 @@ import { } from '../../initializers/constants' import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' import { VideoRedundancyModel } from '../redundancy/video-redundancy' +import { doesExist } from '../shared' import { parseAggregateResult, throwIfNotValid } from '../utils' import { VideoModel } from './video' import { VideoStreamingPlaylistModel } from './video-streaming-playlist' diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 72ba474b4..a2dc7075d 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -17,7 +17,6 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { setAsUpdated } from '@server/helpers/database-utils' import { buildUUID, uuidToShort } from '@server/helpers/uuid' import { MAccountId, MChannelId } from '@server/types/models' import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils' @@ -53,6 +52,7 @@ import { } from '../../types/models/video/video-playlist' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { ActorModel } from '../actor/actor' +import { setAsUpdated } from '../shared' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, @@ -82,6 +82,7 @@ type AvailableForListOptions = { videoChannelId?: number listMyPlaylists?: boolean search?: string + host?: string withVideos?: boolean } @@ -141,9 +142,19 @@ function getVideoLengthSelect () { ] }, [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { + const whereAnd: WhereOptions[] = [] + + const whereServer = options.host && options.host !== WEBSERVER.HOST + ? { host: options.host } + : undefined + let whereActor: WhereOptions = {} - const whereAnd: WhereOptions[] = [] + if (options.host === WEBSERVER.HOST) { + whereActor = { + [Op.and]: [ { serverId: null } ] + } + } if (options.listMyPlaylists !== true) { whereAnd.push({ @@ -168,9 +179,7 @@ function getVideoLengthSelect () { }) } - whereActor = { - [Op.or]: whereActorOr - } + Object.assign(whereActor, { [Op.or]: whereActorOr }) } if (options.accountId) { @@ -228,7 +237,7 @@ function getVideoLengthSelect () { include: [ { model: AccountModel.scope({ - method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] + method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] }), required: true }, @@ -349,6 +358,7 @@ export class VideoPlaylistModel extends Model