Compare commits

...

5 Commits

Author SHA1 Message Date
kakkokari-gtyih c13856e700 fix types 2025-12-17 13:42:10 +09:00
kakkokari-gtyih 143296afed fix: queryとbirthdayを同時に受け付けないように 2025-12-17 13:24:07 +09:00
kakkokari-gtyih 999d965715 Merge remote-tracking branch 'msky/develop' into copilot/add-filter-for-followers 2025-12-17 13:21:13 +09:00
copilot-swe-agent[bot] f40a74449d feat: add query parameter to users/following and users/followers APIs for filtering
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-11-29 09:52:07 +00:00
copilot-swe-agent[bot] 6591199c05 Initial plan 2025-11-29 09:40:10 +00:00
4 changed files with 65 additions and 6 deletions

View File

@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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();

View File

@ -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 = {
@ -86,9 +88,21 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
birthday: { ...birthdaySchema, nullable: true },
},
},
{
oneOf: [{
type: 'object',
properties: {
query: { type: 'string' },
},
}, {
type: 'object',
properties: {
birthday: { ...birthdaySchema, nullable: true },
},
}],
},
],
} as const;
@ -108,6 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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 +161,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee');
if (ps.birthday) {
// query takes priority over birthday
if ('query' in ps && ps.query != null) {
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 ('birthday' in ps && ps.birthday != null) {
try {
const birthday = ps.birthday.substring(5, 10);
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');

View File

@ -25,11 +25,16 @@ const props = defineProps<{
type: 'following' | 'followers';
}>();
const followingPaginator = markRaw(new Paginator('users/following', {
const followingPaginator = markRaw(new Paginator<'users/following', {
req: Misskey.entities.UsersFollowingRequest & { query?: string; };
res: Misskey.entities.UsersFollowingResponse;
}>('users/following', {
limit: 20,
computedParams: computed(() => ({
userId: props.user.id,
})),
canSearch: true,
searchParamName: 'query',
}));
const followersPaginator = markRaw(new Paginator('users/followers', {
@ -37,6 +42,8 @@ const followersPaginator = markRaw(new Paginator('users/followers', {
computedParams: computed(() => ({
userId: props.user.id,
})),
canSearch: true,
searchParamName: 'query',
}));
</script>

View File

@ -34768,6 +34768,7 @@ export interface operations {
untilDate?: number;
/** @default 10 */
limit?: number;
query?: string | null;
};
};
};
@ -34847,8 +34848,11 @@ export interface operations {
untilDate?: number;
/** @default 10 */
limit?: number;
} & ({
query?: string;
} | {
birthday?: string | null;
};
});
};
};
responses: {