From b033851fb54241bb703f86add025229e68cc6f59 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 29 Jul 2021 10:27:24 +0200 Subject: [PATCH] Search channels against handles and not names --- .../api/search/search-video-channels.ts | 2 +- server/helpers/custom-validators/misc.ts | 5 +++ server/middlewares/validators/search.ts | 7 ++-- server/models/video/video-channel.ts | 36 ++++++++++++++----- server/tests/api/check-params/search.ts | 4 +++ server/tests/api/search/search-channels.ts | 21 +++++++---- shared/core-utils/index.ts | 1 + shared/core-utils/utils/index.ts | 1 + shared/core-utils/utils/object.ts | 15 ++++++++ .../extra-utils/moderation/abuses-command.ts | 6 ++-- shared/extra-utils/server/follows-command.ts | 8 ++--- shared/extra-utils/server/jobs-command.ts | 2 +- shared/extra-utils/users/users-command.ts | 3 +- shared/extra-utils/videos/channels-command.ts | 2 +- .../extra-utils/videos/playlists-command.ts | 3 +- shared/extra-utils/videos/videos-command.ts | 3 +- .../video-channels-search-query.model.ts | 2 +- 17 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 shared/core-utils/utils/index.ts create mode 100644 shared/core-utils/utils/object.ts diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts index 9fc2d53a5..ae32a6726 100644 --- a/server/controllers/api/search/search-video-channels.ts +++ b/server/controllers/api/search/search-video-channels.ts @@ -100,7 +100,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr count: query.count, sort: query.sort, host: query.host, - names: query.names + handles: query.handles }, 'filter:api.search.video-channels.local.list.params') const resultList = await Hooks.wrapPromiseFun( diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index f8f168149..c19a3e5eb 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -23,6 +23,10 @@ function isNotEmptyIntArray (value: any) { return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 } +function isNotEmptyStringArray (value: any) { + return Array.isArray(value) && value.every(v => typeof v === 'string' && v.length !== 0) && value.length !== 0 +} + function isArrayOf (value: any, validator: (value: any) => boolean) { return isArray(value) && value.every(v => validator(v)) } @@ -187,6 +191,7 @@ export { isIntOrNull, isIdValid, isSafePath, + isNotEmptyStringArray, isUUIDValid, toCompleteUUIDs, toCompleteUUID, diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index cde300968..27d0e541d 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts @@ -2,7 +2,7 @@ import * as express from 'express' import { query } from 'express-validator' import { isSearchTargetValid } from '@server/helpers/custom-validators/search' import { isHostValid } from '@server/helpers/custom-validators/servers' -import { areUUIDsValid, isDateValid, toCompleteUUIDs } from '../../helpers/custom-validators/misc' +import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc' import { logger } from '../../helpers/logger' import { areValidationErrors } from './shared' @@ -64,9 +64,10 @@ const videoChannelsListSearchValidator = [ .optional() .custom(isSearchTargetValid).withMessage('Should have a valid search target'), - query('names') + query('handles') .optional() - .toArray(), + .toArray() + .custom(isNotEmptyStringArray).withMessage('Should have valid handles'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking video channels search query', { parameters: req.query }) diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 327f49304..e4b12c517 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -18,7 +18,7 @@ import { UpdatedAt } from 'sequelize-typescript' import { MAccountActor } from '@server/types/models' -import { AttributesOnly } from '@shared/core-utils' +import { AttributesOnly, pick } from '@shared/core-utils' import { ActivityPubActor } from '../../../shared/models/activitypub' import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' import { @@ -59,7 +59,7 @@ type AvailableForListOptions = { actorId: number search?: string host?: string - names?: string[] + handles?: string[] } type AvailableWithStatsOptions = { @@ -114,15 +114,33 @@ export type SummaryOptions = { }) } - if (options.names) { - whereActorAnd.push({ - preferredUsername: { - [Op.in]: options.names + let rootWhere: WhereOptions + if (options.handles) { + const or: WhereOptions[] = [] + + for (const handle of options.handles || []) { + const [ preferredUsername, host ] = handle.split('@') + + if (!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: { @@ -473,7 +491,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` sort: string host?: string - names?: string[] + handles?: string[] }) { let attributesInclude: any[] = [ literal('0 as similarity') ] let where: WhereOptions @@ -507,7 +525,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` return VideoChannelModel .scope({ - method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host, names: options.names } as AvailableForListOptions ] + method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] }) .findAndCountAll(query) .then(({ rows, count }) => { diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index 789ea7754..cc15d2593 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts @@ -216,6 +216,10 @@ describe('Test videos API validator', function () { await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) }) + it('Should fail with invalid handles', async function () { + await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + it('Should succeed with the correct parameters', async function () { await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) }) diff --git a/server/tests/api/search/search-channels.ts b/server/tests/api/search/search-channels.ts index ef78c0f67..4485c424e 100644 --- a/server/tests/api/search/search-channels.ts +++ b/server/tests/api/search/search-channels.ts @@ -122,18 +122,25 @@ describe('Test channels search', function () { it('Should filter by names', async function () { { - const body = await command.advancedChannelSearch({ search: { names: [ 'squall_channel', 'zell_channel' ] } }) + const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel' ] } }) + expect(body.total).to.equal(1) + expect(body.data).to.have.lengthOf(1) + expect(body.data[0].displayName).to.equal('Squall channel') + } + + { + const body = await command.advancedChannelSearch({ search: { handles: [ 'chocobozzz_channel' ] } }) + expect(body.total).to.equal(0) + expect(body.data).to.have.lengthOf(0) + } + + { + const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel@' + remoteServer.host ] } }) expect(body.total).to.equal(2) expect(body.data).to.have.lengthOf(2) expect(body.data[0].displayName).to.equal('Squall channel') expect(body.data[1].displayName).to.equal('Zell channel') } - - { - const body = await command.advancedChannelSearch({ search: { names: [ 'chocobozzz_channel' ] } }) - expect(body.total).to.equal(0) - expect(body.data).to.have.lengthOf(0) - } }) after(async function () { diff --git a/shared/core-utils/index.ts b/shared/core-utils/index.ts index 66d50ef93..2a7d4d982 100644 --- a/shared/core-utils/index.ts +++ b/shared/core-utils/index.ts @@ -4,3 +4,4 @@ export * from './i18n' export * from './plugins' export * from './renderer' export * from './users' +export * from './utils' diff --git a/shared/core-utils/utils/index.ts b/shared/core-utils/utils/index.ts new file mode 100644 index 000000000..a71977d88 --- /dev/null +++ b/shared/core-utils/utils/index.ts @@ -0,0 +1 @@ +export * from './object' diff --git a/shared/core-utils/utils/object.ts b/shared/core-utils/utils/object.ts new file mode 100644 index 000000000..7b2bb81d0 --- /dev/null +++ b/shared/core-utils/utils/object.ts @@ -0,0 +1,15 @@ +function pick (object: T, keys: (keyof T)[]) { + const result: Partial = {} + + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + result[key] = object[key] + } + } + + return result +} + +export { + pick +} diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts index 7b3abb056..0db32ba46 100644 --- a/shared/extra-utils/moderation/abuses-command.ts +++ b/shared/extra-utils/moderation/abuses-command.ts @@ -1,4 +1,4 @@ -import { pick } from 'lodash' +import { pick } from '@shared/core-utils' import { AbuseFilter, AbuseMessage, @@ -81,7 +81,7 @@ export class AbusesCommand extends AbstractCommand { searchVideo?: string searchVideoChannel?: string } = {}) { - const toPick = [ + const toPick: (keyof typeof options)[] = [ 'count', 'filter', 'id', @@ -121,7 +121,7 @@ export class AbusesCommand extends AbstractCommand { search?: string state?: AbuseState }) { - const toPick = [ + const toPick: (keyof typeof options)[] = [ 'id', 'search', 'state', diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts index 2b889cf66..01ef6f179 100644 --- a/shared/extra-utils/server/follows-command.ts +++ b/shared/extra-utils/server/follows-command.ts @@ -1,4 +1,4 @@ -import { pick } from 'lodash' +import { pick } from '@shared/core-utils' import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models' import { AbstractCommand, OverrideCommandOptions } from '../shared' import { PeerTubeServer } from './server' @@ -15,8 +15,7 @@ export class FollowsCommand extends AbstractCommand { }) { const path = '/api/v1/server/followers' - const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ] - const query = pick(options, toPick) + const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]) return this.getRequestBody>({ ...options, @@ -38,8 +37,7 @@ export class FollowsCommand extends AbstractCommand { } = {}) { const path = '/api/v1/server/following' - const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ] - const query = pick(options, toPick) + const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]) return this.getRequestBody>({ ...options, diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts index 09a299e5b..c4eb12dc2 100644 --- a/shared/extra-utils/server/jobs-command.ts +++ b/shared/extra-utils/server/jobs-command.ts @@ -1,4 +1,4 @@ -import { pick } from 'lodash' +import { pick } from '@shared/core-utils' import { HttpStatusCode } from '@shared/models' import { Job, JobState, JobType, ResultList } from '../../models' import { AbstractCommand, OverrideCommandOptions } from '../shared' diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts index d66ad15f2..ddd20d041 100644 --- a/shared/extra-utils/users/users-command.ts +++ b/shared/extra-utils/users/users-command.ts @@ -1,4 +1,5 @@ -import { omit, pick } from 'lodash' +import { omit } from 'lodash' +import { pick } from '@shared/core-utils' import { HttpStatusCode, MyUser, diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts index f8eb3f885..255e1d62d 100644 --- a/shared/extra-utils/videos/channels-command.ts +++ b/shared/extra-utils/videos/channels-command.ts @@ -1,4 +1,4 @@ -import { pick } from 'lodash' +import { pick } from '@shared/core-utils' import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models' import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts index 6f329800e..ce23900d3 100644 --- a/shared/extra-utils/videos/playlists-command.ts +++ b/shared/extra-utils/videos/playlists-command.ts @@ -1,4 +1,5 @@ -import { omit, pick } from 'lodash' +import { omit } from 'lodash' +import { pick } from '@shared/core-utils' import { BooleanBothQuery, HttpStatusCode, diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts index 98465e8f6..33725bfdc 100644 --- a/shared/extra-utils/videos/videos-command.ts +++ b/shared/extra-utils/videos/videos-command.ts @@ -3,10 +3,11 @@ import { expect } from 'chai' import { createReadStream, stat } from 'fs-extra' import got, { Response as GotResponse } from 'got' -import { omit, pick } from 'lodash' +import { omit } from 'lodash' import validator from 'validator' import { buildUUID } from '@server/helpers/uuid' import { loadLanguages } from '@server/initializers/constants' +import { pick } from '@shared/core-utils' import { HttpStatusCode, ResultList, diff --git a/shared/models/search/video-channels-search-query.model.ts b/shared/models/search/video-channels-search-query.model.ts index 50c59d41d..77cea4a59 100644 --- a/shared/models/search/video-channels-search-query.model.ts +++ b/shared/models/search/video-channels-search-query.model.ts @@ -8,5 +8,5 @@ export interface VideoChannelsSearchQuery extends SearchTargetQuery { sort?: string host?: string - names?: string[] + handles?: string[] }