diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 84c4c80d01..3c55026208 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsNull } from 'typeorm'; +import { Brackets, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -12,6 +12,8 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -79,6 +81,7 @@ export const paramDef = { sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + query: { type: 'string', nullable: true }, }, }, ], @@ -100,6 +103,7 @@ export default class extends Endpoint { // eslint- private followingEntityService: FollowingEntityService, private queryService: QueryService, private roleService: RoleService, + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy('userId' in ps @@ -138,6 +142,21 @@ export default class extends Endpoint { // eslint- .andWhere('following.followeeId = :userId', { userId: user.id }) .innerJoinAndSelect('following.follower', 'follower'); + if (ps.query) { + const searchQuery = ps.query; + const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1; + + query.andWhere(new Brackets(qb => { + qb.where('follower.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' }); + + if (isUsername) { + qb.orWhere('follower.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' }); + } else if (this.userEntityService.validateLocalUsername(searchQuery)) { + qb.orWhere('follower.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' }); + } + })); + } + const followings = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 047f9a053b..6697e2d09d 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsNull } from 'typeorm'; +import { Brackets, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; import { birthdaySchema } from '@/models/User.js'; @@ -13,6 +13,8 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -87,6 +89,7 @@ export const paramDef = { untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, birthday: { ...birthdaySchema, nullable: true }, + query: { type: 'string', nullable: true }, }, }, ], @@ -108,6 +111,7 @@ export default class extends Endpoint { // eslint- private followingEntityService: FollowingEntityService, private queryService: QueryService, private roleService: RoleService, + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy('userId' in ps @@ -146,7 +150,21 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); - if (ps.birthday) { + // query takes priority over birthday + if (ps.query) { + const searchQuery = ps.query; + const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1; + + query.andWhere(new Brackets(qb => { + qb.where('followee.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' }); + + if (isUsername) { + qb.orWhere('followee.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' }); + } else if (this.userEntityService.validateLocalUsername(searchQuery)) { + qb.orWhere('followee.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' }); + } + })); + } else if (ps.birthday) { try { const birthday = ps.birthday.substring(5, 10); const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue index c383b9b7bd..0b13d8e6cb 100644 --- a/packages/frontend/src/pages/user/follow-list.vue +++ b/packages/frontend/src/pages/user/follow-list.vue @@ -30,6 +30,8 @@ const followingPaginator = markRaw(new Paginator('users/following', { computedParams: computed(() => ({ userId: props.user.id, })), + canSearch: true, + searchParamName: 'query', })); const followersPaginator = markRaw(new Paginator('users/followers', { @@ -37,6 +39,8 @@ const followersPaginator = markRaw(new Paginator('users/followers', { computedParams: computed(() => ({ userId: props.user.id, })), + canSearch: true, + searchParamName: 'query', })); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 2650869590..254f1bcd73 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -34768,6 +34768,7 @@ export interface operations { untilDate?: number; /** @default 10 */ limit?: number; + query?: string | null; }; }; }; @@ -34848,6 +34849,7 @@ export interface operations { /** @default 10 */ limit?: number; birthday?: string | null; + query?: string | null; }; }; };