From c29a5764d3836bc05906036be20fab2eeea8b85b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 27 Mar 2025 16:51:08 +0900
Subject: [PATCH 1/4] refactor(backend): better method name

---
 packages/backend/src/core/QueryService.ts                     | 4 ++--
 packages/backend/src/core/SearchService.ts                    | 4 ++--
 packages/backend/src/server/api/endpoints/antennas/notes.ts   | 4 ++--
 .../backend/src/server/api/endpoints/channels/timeline.ts     | 4 ++--
 packages/backend/src/server/api/endpoints/clips/notes.ts      | 4 ++--
 packages/backend/src/server/api/endpoints/notes/children.ts   | 4 ++--
 .../backend/src/server/api/endpoints/notes/global-timeline.ts | 4 ++--
 .../backend/src/server/api/endpoints/notes/hybrid-timeline.ts | 4 ++--
 .../backend/src/server/api/endpoints/notes/local-timeline.ts  | 4 ++--
 packages/backend/src/server/api/endpoints/notes/mentions.ts   | 4 ++--
 packages/backend/src/server/api/endpoints/notes/renotes.ts    | 4 ++--
 packages/backend/src/server/api/endpoints/notes/replies.ts    | 4 ++--
 .../backend/src/server/api/endpoints/notes/search-by-tag.ts   | 4 ++--
 packages/backend/src/server/api/endpoints/notes/timeline.ts   | 4 ++--
 .../src/server/api/endpoints/notes/user-list-timeline.ts      | 4 ++--
 packages/backend/src/server/api/endpoints/roles/notes.ts      | 4 ++--
 packages/backend/src/server/api/endpoints/users/notes.ts      | 4 ++--
 .../backend/src/server/api/endpoints/users/recommendation.ts  | 2 +-
 18 files changed, 35 insertions(+), 35 deletions(-)

diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index c4feeaf971..412ab33b3f 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -69,7 +69,7 @@ export class QueryService {
 
 	// ここでいうBlockedは被Blockedの意
 	@bindThis
-	public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
+	public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
 		const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
 			.select('blocking.blockerId')
 			.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
@@ -127,7 +127,7 @@ export class QueryService {
 	}
 
 	@bindThis
-	public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
+	public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
 		const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
 			.select('muting.muteeId')
 			.where('muting.muterId = :muterId', { muterId: me.id });
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index bc62559e46..aa787c93de 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -234,8 +234,8 @@ export class SearchService {
 		}
 
 		this.queryService.generateVisibilityQuery(query, me);
-		if (me) this.queryService.generateMutedUserQuery(query, me);
-		if (me) this.queryService.generateBlockedUserQuery(query, me);
+		if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+		if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 		return query.limit(pagination.limit).getMany();
 	}
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 727697ea14..4b8543c2d1 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -109,8 +109,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			this.queryService.generateMutedUserQuery(query, me);
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateMutedUserQueryForNotes(query, me);
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			const notes = await query.getMany();
 			if (sinceId != null && untilId == null) {
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index d4fd75e049..cec5f8fd9c 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -122,8 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			.leftJoinAndSelect('note.channel', 'channel');
 
 		if (me) {
-			this.queryService.generateMutedUserQuery(query, me);
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateMutedUserQueryForNotes(query, me);
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 		}
 		//#endregion
 
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 943c31c894..7638aae442 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -87,8 +87,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (me) {
 				this.queryService.generateVisibilityQuery(query, me);
-				this.queryService.generateMutedUserQuery(query, me);
-				this.queryService.generateBlockedUserQuery(query, me);
+				this.queryService.generateMutedUserQueryForNotes(query, me);
+				this.queryService.generateBlockedUserQueryForNotes(query, me);
 			}
 
 			const notes = await query
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index 0c6533d336..e73c98282c 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			this.queryService.generateVisibilityQuery(query, me);
 			if (me) {
-				this.queryService.generateMutedUserQuery(query, me);
-				this.queryService.generateBlockedUserQuery(query, me);
+				this.queryService.generateMutedUserQueryForNotes(query, me);
+				this.queryService.generateBlockedUserQueryForNotes(query, me);
 			}
 
 			const notes = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 258a0bfb8f..8d38bb1c65 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -79,8 +79,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			if (me) {
-				this.queryService.generateMutedUserQuery(query, me);
-				this.queryService.generateBlockedUserQuery(query, me);
+				this.queryService.generateMutedUserQueryForNotes(query, me);
+				this.queryService.generateBlockedUserQueryForNotes(query, me);
 				this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index aed9065bf9..99d1c9f19c 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -243,8 +243,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		}
 
 		this.queryService.generateVisibilityQuery(query, me);
-		this.queryService.generateMutedUserQuery(query, me);
-		this.queryService.generateBlockedUserQuery(query, me);
+		this.queryService.generateMutedUserQueryForNotes(query, me);
+		this.queryService.generateBlockedUserQueryForNotes(query, me);
 		this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 
 		if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index 0b48f2c78b..97acf2ad39 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -156,8 +156,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			.leftJoinAndSelect('renote.user', 'renoteUser');
 
 		this.queryService.generateVisibilityQuery(query, me);
-		if (me) this.queryService.generateMutedUserQuery(query, me);
-		if (me) this.queryService.generateBlockedUserQuery(query, me);
+		if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+		if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
 		if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 
 		if (ps.withFiles) {
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index 18a3915ab5..bbb63646e9 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -72,9 +72,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			this.queryService.generateMutedUserQuery(query, me);
+			this.queryService.generateMutedUserQueryForNotes(query, me);
 			this.queryService.generateMutedNoteThreadQuery(query, me);
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			if (ps.visibility) {
 				query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index ffe1ee6eb8..b34d9261a1 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -72,8 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			if (me) this.queryService.generateMutedUserQuery(query, me);
-			if (me) this.queryService.generateBlockedUserQuery(query, me);
+			if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+			if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			const renotes = await query.limit(ps.limit).getMany();
 
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index 5f32332a6a..f36af1a328 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -56,8 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			if (me) this.queryService.generateMutedUserQuery(query, me);
-			if (me) this.queryService.generateBlockedUserQuery(query, me);
+			if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+			if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			const timeline = await query.limit(ps.limit).getMany();
 
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 626ff080c7..c45851548a 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -81,8 +81,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			if (me) this.queryService.generateMutedUserQuery(query, me);
-			if (me) this.queryService.generateBlockedUserQuery(query, me);
+			if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+			if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			try {
 				if (ps.tag) {
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 7cb11cc1eb..a88b28892e 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -199,8 +199,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		}));
 
 		this.queryService.generateVisibilityQuery(query, me);
-		this.queryService.generateMutedUserQuery(query, me);
-		this.queryService.generateBlockedUserQuery(query, me);
+		this.queryService.generateMutedUserQueryForNotes(query, me);
+		this.queryService.generateBlockedUserQueryForNotes(query, me);
 		this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 
 		if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 87f9b322a6..80f1c69b25 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -184,8 +184,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}));
 
 		this.queryService.generateVisibilityQuery(query, me);
-		this.queryService.generateMutedUserQuery(query, me);
-		this.queryService.generateBlockedUserQuery(query, me);
+		this.queryService.generateMutedUserQueryForNotes(query, me);
+		this.queryService.generateBlockedUserQueryForNotes(query, me);
 		this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 
 		if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index 71f2782a5d..6cd9f80929 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateVisibilityQuery(query, me);
-			this.queryService.generateMutedUserQuery(query, me);
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateMutedUserQueryForNotes(query, me);
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			const notes = await query.getMany();
 			notes.sort((a, b) => a.id > b.id ? -1 : 1);
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index e9c334057e..f5b7a07b01 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -185,8 +185,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 		this.queryService.generateVisibilityQuery(query, me);
 		if (me) {
-			this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 		}
 
 		if (ps.withFiles) {
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
index 5b3b4527f7..5b1c6b514b 100644
--- a/packages/backend/src/server/api/endpoints/users/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -63,7 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			this.queryService.generateMutedUserQueryForUsers(query, me);
 			this.queryService.generateBlockQueryForUsers(query, me);
-			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateBlockedUserQueryForNotes(query, me);
 
 			const followingQuery = this.followingsRepository.createQueryBuilder('following')
 				.select('following.followeeId')

From b95da9c9a46d55c673787ed78526afbc9107785c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 27 Mar 2025 17:12:23 +0900
Subject: [PATCH 2/4] =?UTF-8?q?enhance(backend):=20=E3=83=9F=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E3=83=A6?=
 =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=82=92=E3=83=A6=E3=83=BC=E3=82=B6?=
 =?UTF-8?q?=E3=83=BC=E6=A4=9C=E7=B4=A2=E3=81=AE=E7=B5=90=E6=9E=9C=E3=81=8B?=
 =?UTF-8?q?=E3=82=89=E9=99=A4=E5=A4=96=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |   1 +
 .../backend/src/core/UserSearchService.ts     | 100 +++++++++++++++++-
 .../users/search-by-username-and-host.ts      |   2 +-
 .../src/server/api/endpoints/users/search.ts  |  81 ++------------
 4 files changed, 107 insertions(+), 77 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff95b6b5dc..6d94dbe213 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
   - メッセージにはリアクションも可能です
 - Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
   - Misskeyネイティブでダッシュボードを実装予定です
+- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
 
 ### Client
 - Feat: 設定の管理が強化されました
diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts
index 0d03cf6ee0..4be7bd9bdb 100644
--- a/packages/backend/src/core/UserSearchService.ts
+++ b/packages/backend/src/core/UserSearchService.ts
@@ -6,7 +6,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { Brackets, SelectQueryBuilder } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
+import { type FollowingsRepository, MiUser, type MutingsRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
 import type { Config } from '@/config.js';
@@ -22,10 +22,19 @@ export class UserSearchService {
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
+
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
+
+		@Inject(DI.mutingsRepository)
+		private mutingsRepository: MutingsRepository,
+
 		private userEntityService: UserEntityService,
 	) {
 	}
@@ -58,7 +67,7 @@ export class UserSearchService {
 	 * @see {@link UserSearchService#buildSearchUserNoLoginQueries}
 	 */
 	@bindThis
-	public async search(
+	public async searchByUsernameAndHost(
 		params: {
 			username?: string | null,
 			host?: string | null,
@@ -202,4 +211,91 @@ export class UserSearchService {
 
 		return userQuery;
 	}
+
+	@bindThis
+	public async search(query: string, meId: MiUser['id'] | null, options: Partial<{
+		limit: number;
+		offset: number;
+		origin: 'local' | 'remote' | 'combined';
+	}> = {}) {
+		const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
+		const isUsername = query.startsWith('@') && !query.includes(' ') && query.indexOf('@', 1) === -1;
+
+		let users: MiUser[] = [];
+
+		const mutingQuery = meId == null ? null : this.mutingsRepository.createQueryBuilder('muting')
+			.select('muting.muteeId')
+			.where('muting.muterId = :muterId', { muterId: meId });
+
+		const nameQuery = this.usersRepository.createQueryBuilder('user')
+			.where(new Brackets(qb => {
+				qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
+
+				if (isUsername) {
+					qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(query.replace('@', '').toLowerCase()) + '%' });
+				} else if (this.userEntityService.validateLocalUsername(query)) { // Also search username if it qualifies as username
+					qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(query.toLowerCase()) + '%' });
+				}
+			}))
+			.andWhere(new Brackets(qb => {
+				qb
+					.where('user.updatedAt IS NULL')
+					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+			}))
+			.andWhere('user.isSuspended = FALSE');
+
+		if (mutingQuery) {
+			nameQuery.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`);
+			nameQuery.setParameters(mutingQuery.getParameters());
+		}
+
+		if (options.origin === 'local') {
+			nameQuery.andWhere('user.host IS NULL');
+		} else if (options.origin === 'remote') {
+			nameQuery.andWhere('user.host IS NOT NULL');
+		}
+
+		users = await nameQuery
+			.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+			.limit(options.limit)
+			.offset(options.offset)
+			.getMany();
+
+		if (users.length < (options.limit ?? 30)) {
+			const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
+				.select('prof.userId')
+				.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
+
+			if (mutingQuery) {
+				profQuery.andWhere(`prof.userId NOT IN (${mutingQuery.getQuery()})`);
+				profQuery.setParameters(mutingQuery.getParameters());
+			}
+
+			if (options.origin === 'local') {
+				profQuery.andWhere('prof.userHost IS NULL');
+			} else if (options.origin === 'remote') {
+				profQuery.andWhere('prof.userHost IS NOT NULL');
+			}
+
+			const userQuery = this.usersRepository.createQueryBuilder('user')
+				.where(`user.id IN (${ profQuery.getQuery() })`)
+				.andWhere(new Brackets(qb => {
+					qb
+						.where('user.updatedAt IS NULL')
+						.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+				}))
+				.andWhere('user.isSuspended = FALSE')
+				.setParameters(profQuery.getParameters());
+
+			users = users.concat(await userQuery
+				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+				.limit(options.limit)
+				.offset(options.offset)
+				.getMany(),
+			);
+		}
+
+		return users;
+	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 8ff952dcb5..134f1a8e87 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private userSearchService: UserSearchService,
 	) {
 		super(meta, paramDef, (ps, me) => {
-			return this.userSearchService.search({
+			return this.userSearchService.searchByUsernameAndHost({
 				username: ps.username,
 				host: ps.host,
 			}, {
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index 0b0136066d..5d36847e03 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -3,14 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Brackets } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
-import type { MiUser } from '@/models/User.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { DI } from '@/di-symbols.js';
-import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
 
 export const meta = {
 	tags: ['users'],
@@ -45,79 +42,15 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.usersRepository)
-		private usersRepository: UsersRepository,
-
-		@Inject(DI.userProfilesRepository)
-		private userProfilesRepository: UserProfilesRepository,
-
 		private userEntityService: UserEntityService,
+		private userSearchService: UserSearchService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
-			ps.query = ps.query.trim();
-			const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
-
-			let users: MiUser[] = [];
-
-			const nameQuery = this.usersRepository.createQueryBuilder('user')
-				.where(new Brackets(qb => {
-					qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
-					if (isUsername) {
-						qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
-					} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
-						qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
-					}
-				}))
-				.andWhere(new Brackets(qb => {
-					qb
-						.where('user.updatedAt IS NULL')
-						.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-				}))
-				.andWhere('user.isSuspended = FALSE');
-
-			if (ps.origin === 'local') {
-				nameQuery.andWhere('user.host IS NULL');
-			} else if (ps.origin === 'remote') {
-				nameQuery.andWhere('user.host IS NOT NULL');
-			}
-
-			users = await nameQuery
-				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-				.limit(ps.limit)
-				.offset(ps.offset)
-				.getMany();
-
-			if (users.length < ps.limit) {
-				const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
-					.select('prof.userId')
-					.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
-				if (ps.origin === 'local') {
-					profQuery.andWhere('prof.userHost IS NULL');
-				} else if (ps.origin === 'remote') {
-					profQuery.andWhere('prof.userHost IS NOT NULL');
-				}
-
-				const query = this.usersRepository.createQueryBuilder('user')
-					.where(`user.id IN (${ profQuery.getQuery() })`)
-					.andWhere(new Brackets(qb => {
-						qb
-							.where('user.updatedAt IS NULL')
-							.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-					}))
-					.andWhere('user.isSuspended = FALSE')
-					.setParameters(profQuery.getParameters());
-
-				users = users.concat(await query
-					.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-					.limit(ps.limit)
-					.offset(ps.offset)
-					.getMany(),
-				);
-			}
+			const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, {
+				offset: ps.offset,
+				limit: ps.limit,
+				origin: ps.origin,
+			});
 
 			return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
 		});

From f7e901deb2a3d7457231c9a240d13d332e24461a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 27 Mar 2025 17:30:04 +0900
Subject: [PATCH 3/4] test fixes

---
 .../backend/test/unit/UserSearchService.ts    | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts
index 66a7f39ff1..697425beb8 100644
--- a/packages/backend/test/unit/UserSearchService.ts
+++ b/packages/backend/test/unit/UserSearchService.ts
@@ -134,13 +134,13 @@ describe('UserSearchService', () => {
 		await app.close();
 	});
 
-	describe('search', () => {
+	describe('searchByUsernameAndHost', () => {
 		test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
 			await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
 			await setInactive([alycia, alysha, alyson]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 				root,
@@ -154,7 +154,7 @@ describe('UserSearchService', () => {
 			await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 				root,
@@ -168,7 +168,7 @@ describe('UserSearchService', () => {
 			await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setInactive([alice, alyce, alycia]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 				root,
@@ -181,7 +181,7 @@ describe('UserSearchService', () => {
 		test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
 			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 				root,
@@ -195,7 +195,7 @@ describe('UserSearchService', () => {
 			await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
 			await setInactive([alyson, alice, alysha, bobbie, bobby]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ },
 				{ limit: 100 },
 				root,
@@ -216,7 +216,7 @@ describe('UserSearchService', () => {
 			await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setInactive([alice, alyce, alycia]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 			);
@@ -228,7 +228,7 @@ describe('UserSearchService', () => {
 		test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
 			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 			);
@@ -240,7 +240,7 @@ describe('UserSearchService', () => {
 			await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al', host: 'exam' },
 				{ limit: 100 },
 				root,
@@ -253,7 +253,7 @@ describe('UserSearchService', () => {
 			await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
 			await setSuspended([alice, alyce, alycia]);
 
-			const result = await service.search(
+			const result = await service.searchByUsernameAndHost(
 				{ username: 'al' },
 				{ limit: 100 },
 				root,

From a78db27a3ca4e2125b217bdef4a0920d33029423 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 27 Mar 2025 17:30:06 +0900
Subject: [PATCH 4/4] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d94dbe213..ff77553206 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@
   - 投稿フォームをリセットできるように
   - 文字数カウントを復活
 - Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
+- Enhance: 全体的なブラッシュアップ
 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正
 
 ### Server