forked from mirrors/misskey
Merge branch 'develop' into mahjong
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
@@ -60,6 +61,7 @@ import { UserFollowingService } from './UserFollowingService.js';
|
||||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
@@ -203,6 +205,8 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
|
||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
|
||||
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
@@ -350,6 +354,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
@@ -493,6 +499,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
@@ -637,6 +645,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
@@ -779,6 +789,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
|
||||
@@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
||||
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -256,6 +256,10 @@ type SerializedAll<T> = {
|
||||
[K in keyof T]: Serialized<T[K]>;
|
||||
};
|
||||
|
||||
type UndefinedAsNullAll<T> = {
|
||||
[K in keyof T]: T[K] extends undefined ? null : T[K];
|
||||
}
|
||||
|
||||
export interface InternalEventTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
||||
@@ -294,43 +298,45 @@ export interface InternalEventTypes {
|
||||
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||
}
|
||||
|
||||
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
|
||||
|
||||
// name/messages(spec) pairs dictionary
|
||||
export type GlobalEvents = {
|
||||
internal: {
|
||||
name: 'internal';
|
||||
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
|
||||
payload: EventTypesToEventPayload<InternalEventTypes>;
|
||||
};
|
||||
broadcast: {
|
||||
name: 'broadcast';
|
||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||
payload: EventTypesToEventPayload<BroadcastTypes>;
|
||||
};
|
||||
main: {
|
||||
name: `mainStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
|
||||
payload: EventTypesToEventPayload<MainEventTypes>;
|
||||
};
|
||||
drive: {
|
||||
name: `driveStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
|
||||
payload: EventTypesToEventPayload<DriveEventTypes>;
|
||||
};
|
||||
note: {
|
||||
name: `noteStream:${MiNote['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||
payload: EventTypesToEventPayload<NoteStreamEventTypes>;
|
||||
};
|
||||
userList: {
|
||||
name: `userListStream:${MiUserList['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||
payload: EventTypesToEventPayload<UserListEventTypes>;
|
||||
};
|
||||
roleTimeline: {
|
||||
name: `roleTimelineStream:${MiRole['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
|
||||
payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
|
||||
};
|
||||
antenna: {
|
||||
name: `antennaStream:${MiAntenna['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AntennaEventTypes>;
|
||||
};
|
||||
admin: {
|
||||
name: `adminStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AdminEventTypes>;
|
||||
};
|
||||
notes: {
|
||||
name: 'notesStream';
|
||||
@@ -338,11 +344,11 @@ export type GlobalEvents = {
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
};
|
||||
reversiGame: {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
|
||||
};
|
||||
mahjongRoom: {
|
||||
name: `mahjongRoomStream:${string}`;
|
||||
|
||||
@@ -933,10 +933,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
// 自分自身のHTL
|
||||
if (note.userHost == null) {
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export type RolePolicies = {
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
canUpdateBioMedia: boolean;
|
||||
pinLimit: number;
|
||||
antennaLimit: number;
|
||||
wordMuteLimit: number;
|
||||
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
alwaysMarkNsfw: false,
|
||||
canUpdateBioMedia: true,
|
||||
pinLimit: 5,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||
@@ -502,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
|
||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||
|
||||
if (role.isPublic) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
|
||||
if (role.isPublic && user.host === null) {
|
||||
this.notificationService.createNotification(userId, 'roleAssigned', {
|
||||
roleId: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
if (moderator) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
this.moderationLogService.log(moderator, 'assignRole', {
|
||||
roleId: roleId,
|
||||
roleName: role.name,
|
||||
|
||||
@@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
|
||||
});
|
||||
|
||||
// 通知を作成
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
if (follower.host === null) {
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
||||
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal file
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserRenoteMutingService {
|
||||
constructor(
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
|
||||
await this.renoteMutingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
muterId: user.id,
|
||||
muteeId: target.id,
|
||||
});
|
||||
|
||||
await this.cacheService.renoteMutingsCache.refresh(user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
|
||||
if (mutings.length === 0) return;
|
||||
|
||||
await this.renoteMutingsRepository.delete({
|
||||
id: In(mutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
const muterIds = [...new Set(mutings.map(m => m.muterId))];
|
||||
for (const muterId of muterIds) {
|
||||
await this.cacheService.renoteMutingsCache.refresh(muterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
205
packages/backend/src/core/UserSearchService.ts
Normal file
205
packages/backend/src/core/UserSearchService.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 { bindThis } from '@/decorators.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
function defaultActiveThreshold() {
|
||||
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserSearchService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザ名とホスト名によるユーザ検索を行う.
|
||||
*
|
||||
* - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
|
||||
* 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
|
||||
* 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
|
||||
* 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
|
||||
* 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
|
||||
* - ログインしていない場合は、以下の順序で検索が行われる.
|
||||
* 1. 一定期間以内に更新されたユーザ
|
||||
* 2. 一定期間以内に更新されていないユーザ
|
||||
* - それぞれの検索結果はユーザ名の昇順でソートされる.
|
||||
* - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
|
||||
* (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
|
||||
* - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
|
||||
* - ユーザ名の検索は大文字小文字を区別しない.
|
||||
* - ホスト名の検索は大文字小文字を区別しない.
|
||||
* - 検索結果は最大で {@link opts.limit} 件までとなる.
|
||||
*
|
||||
* ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
|
||||
*
|
||||
* @param params 検索条件.
|
||||
* @param opts 関数の動作を制御するオプション.
|
||||
* @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
|
||||
* @see {@link UserSearchService#buildSearchUserQueries}
|
||||
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||
*/
|
||||
@bindThis
|
||||
public async search(
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
opts?: {
|
||||
limit?: number,
|
||||
detail?: boolean,
|
||||
},
|
||||
me?: MiUser | null,
|
||||
): Promise<Packed<'User'>[]> {
|
||||
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
|
||||
|
||||
let resultSet = new Set<MiUser['id']>();
|
||||
const limit = opts?.limit ?? 10;
|
||||
for (const query of queries) {
|
||||
const ids = await query
|
||||
.select('user.id')
|
||||
.limit(limit - resultSet.size)
|
||||
.orderBy('user.usernameLower', 'ASC')
|
||||
.getRawMany<{ user_id: MiUser['id'] }>()
|
||||
.then(res => res.map(x => x.user_id));
|
||||
|
||||
resultSet = new Set([...resultSet, ...ids]);
|
||||
if (resultSet.size >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
|
||||
[...resultSet].slice(0, limit),
|
||||
me,
|
||||
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param me
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserQueries(
|
||||
me: MiUser,
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
// 自分自身がヒットするとしたらここ
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserNoLoginQueries(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
}) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
|
||||
return [activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private generateUserQueryBuilder(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
}): SelectQueryBuilder<MiUser> {
|
||||
const userQuery = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
if (params.username) {
|
||||
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
|
||||
}
|
||||
|
||||
if (params.host) {
|
||||
if (params.host === this.config.hostname || params.host === '.') {
|
||||
userQuery.andWhere('user.host IS NULL');
|
||||
} else {
|
||||
userQuery.andWhere('user.host LIKE :host', {
|
||||
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
userQuery.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
return userQuery;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class ApMfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getNoteHtml(note: MiNote, apAppend?: string) {
|
||||
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
|
||||
let noMisskeyContent = false;
|
||||
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js';
|
||||
import type { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
@@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
|
||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||
}));
|
||||
|
||||
if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
|
||||
&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/*
|
||||
we don't want to return nulls on errors! if the database fields
|
||||
are already null, nothing changes; if the database has old
|
||||
|
||||
@@ -74,10 +74,10 @@ export class ApQuestionService {
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const note = await this.notesRepository.findOneBy({ uri });
|
||||
if (note == null) throw new Error('Question is not registed');
|
||||
if (note == null) throw new Error('Question is not registered');
|
||||
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
if (poll == null) throw new Error('Question is not registed');
|
||||
if (poll == null) throw new Error('Question is not registered');
|
||||
//#endregion
|
||||
|
||||
// resolve new Question object
|
||||
|
||||
@@ -50,6 +50,22 @@ export class MetaEntityService {
|
||||
}))
|
||||
.getMany();
|
||||
|
||||
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
||||
let defaultLightTheme = null;
|
||||
let defaultDarkTheme = null;
|
||||
if (instance.defaultLightTheme) {
|
||||
try {
|
||||
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
if (instance.defaultDarkTheme) {
|
||||
try {
|
||||
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const packed: Packed<'MetaLite'> = {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
@@ -90,9 +106,8 @@ export class MetaEntityService {
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
||||
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
|
||||
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
|
||||
defaultLightTheme,
|
||||
defaultDarkTheme,
|
||||
ads: ads.map(ad => ({
|
||||
id: ad.id,
|
||||
url: ad.url,
|
||||
|
||||
@@ -501,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
// パフォーマンス上の理由でローカルユーザーのみ
|
||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
}))) : undefined,
|
||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||
.filter((r) => r.isPublic || iAmModerator)
|
||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||
.map((r) => ({
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
}))
|
||||
) : undefined,
|
||||
|
||||
...(isDetailed ? {
|
||||
url: profile!.url,
|
||||
|
||||
Reference in New Issue
Block a user