mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-04-14 15:33:20 +09:00
enhance(backend): ミュートしているユーザーをユーザー検索の結果から除外するように
This commit is contained in:
parent
c29a5764d3
commit
b95da9c9a4
@ -13,6 +13,7 @@
|
|||||||
- メッセージにはリアクションも可能です
|
- メッセージにはリアクションも可能です
|
||||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
||||||
- Misskeyネイティブでダッシュボードを実装予定です
|
- Misskeyネイティブでダッシュボードを実装予定です
|
||||||
|
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Feat: 設定の管理が強化されました
|
- Feat: 設定の管理が強化されました
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { bindThis } from '@/decorators.js';
|
||||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@ -22,10 +22,19 @@ export class UserSearchService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutingsRepository)
|
||||||
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@ -58,7 +67,7 @@ export class UserSearchService {
|
|||||||
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async search(
|
public async searchByUsernameAndHost(
|
||||||
params: {
|
params: {
|
||||||
username?: string | null,
|
username?: string | null,
|
||||||
host?: string | null,
|
host?: string | null,
|
||||||
@ -202,4 +211,91 @@ export class UserSearchService {
|
|||||||
|
|
||||||
return userQuery;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private userSearchService: UserSearchService,
|
private userSearchService: UserSearchService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, (ps, me) => {
|
super(meta, paramDef, (ps, me) => {
|
||||||
return this.userSearchService.search({
|
return this.userSearchService.searchByUsernameAndHost({
|
||||||
username: ps.username,
|
username: ps.username,
|
||||||
host: ps.host,
|
host: ps.host,
|
||||||
}, {
|
}, {
|
||||||
|
@ -3,14 +3,11 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
@ -45,79 +42,15 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private userSearchService: UserSearchService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, {
|
||||||
|
offset: ps.offset,
|
||||||
ps.query = ps.query.trim();
|
limit: ps.limit,
|
||||||
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
|
origin: ps.origin,
|
||||||
|
});
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
|
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user