From 36d712699206f0e197d589a55097b1544db2f62f Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 01:31:13 -0400 Subject: [PATCH 01/31] implement InstanceEntityService.fetchInstancesByHost --- .../core/entities/InstanceEntityService.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index a2ee4b0505..4ca4ff650b 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; import { bindThis } from '@/decorators.js'; @@ -11,7 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { RoleService } from '@/core/RoleService.js'; import { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MiMeta } from '@/models/_.js'; +import type { InstancesRepository, MiMeta } from '@/models/_.js'; @Injectable() export class InstanceEntityService { @@ -19,6 +20,9 @@ export class InstanceEntityService { @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.instancesRepository) + private readonly instancesRepository: InstancesRepository, + private roleService: RoleService, private utilityService: UtilityService, @@ -73,5 +77,28 @@ export class InstanceEntityService { ) { return Promise.all(instances.map(x => this.pack(x, me))); } + + @bindThis + public async fetchInstancesByHost(instances: (MiInstance | MiInstance['host'])[]): Promise { + const result: MiInstance[] = []; + + const toFetch: string[] = []; + for (const instance of instances) { + if (typeof(instance) === 'string') { + toFetch.push(instance); + } else { + result.push(instance); + } + } + + if (toFetch.length > 0) { + const fetched = await this.instancesRepository.findBy({ + host: In(toFetch), + }); + result.push(...fetched); + } + + return result; + } } From cfbf2c9c8eb374791d163962b761248b11dc557e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 01:32:45 -0400 Subject: [PATCH 02/31] pack users with "me" context in AbuseUserReportEntityService --- .../core/entities/AbuseUserReportEntityService.ts | 13 ++++++++----- .../api/endpoints/admin/abuse-user-reports.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 70ead890ab..a6c00d89b9 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { AbuseUserReportsRepository, MiUser } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; @@ -32,9 +32,11 @@ export class AbuseUserReportEntityService { packedTargetUser?: Packed<'UserDetailedNotMe'>, packedAssignee?: Packed<'UserDetailedNotMe'>, }, + me?: MiUser | null, ) { const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); + // noinspection ES6MissingAwait return await awaitAll({ id: report.id, createdAt: this.idService.parse(report.id).date.toISOString(), @@ -43,10 +45,10 @@ export class AbuseUserReportEntityService { reporterId: report.reporterId, targetUserId: report.targetUserId, assigneeId: report.assigneeId, - reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, { + reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, me, { schema: 'UserDetailedNotMe', }), - targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { + targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, me, { schema: 'UserDetailedNotMe', }), assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { @@ -61,13 +63,14 @@ export class AbuseUserReportEntityService { @bindThis public async packMany( reports: MiAbuseUserReport[], + me?: MiUser | null, ) { const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null); const _userMap = await this.userEntityService.packMany( [..._reporters, ..._targetUsers, ..._assignees], - null, + me, { schema: 'UserDetailedNotMe' }, ).then(users => new Map(users.map(u => [u.id, u]))); return Promise.all( @@ -75,7 +78,7 @@ export class AbuseUserReportEntityService { const packedReporter = _userMap.get(report.reporterId); const packedTargetUser = _userMap.get(report.targetUserId); const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined; - return this.pack(report, { packedReporter, packedTargetUser, packedAssignee }); + return this.pack(report, { packedReporter, packedTargetUser, packedAssignee }, me); }), ); } diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 0dbfaae054..ad0bbb5647 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -134,7 +134,7 @@ export default class extends Endpoint { // eslint- const reports = await query.limit(ps.limit).getMany(); - return await this.abuseUserReportEntityService.packMany(reports); + return await this.abuseUserReportEntityService.packMany(reports, me); }); } } From 71f60d519b6f8e655737136484658ea0f1d00507 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 01:33:55 -0400 Subject: [PATCH 03/31] add targetInstance to abuse report schema --- .../entities/AbuseUserReportEntityService.ts | 27 ++++++++++++++++--- .../backend/src/models/AbuseUserReport.ts | 21 +++++++++++++++ .../api/endpoints/admin/abuse-user-reports.ts | 5 ++++ packages/misskey-js/src/autogen/types.ts | 1 + 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index a6c00d89b9..c1d877aa12 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -5,13 +5,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AbuseUserReportsRepository, MiUser } from '@/models/_.js'; +import type { AbuseUserReportsRepository, InstancesRepository, MiInstance, MiUser } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; +import { InstanceEntityService } from './InstanceEntityService.js'; @Injectable() export class AbuseUserReportEntityService { @@ -19,6 +20,10 @@ export class AbuseUserReportEntityService { @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private readonly instanceEntityService: InstanceEntityService, private userEntityService: UserEntityService, private idService: IdService, ) { @@ -30,6 +35,7 @@ export class AbuseUserReportEntityService { hint?: { packedReporter?: Packed<'UserDetailedNotMe'>, packedTargetUser?: Packed<'UserDetailedNotMe'>, + packedTargetInstance?: Packed<'FederationInstance'>, packedAssignee?: Packed<'UserDetailedNotMe'>, }, me?: MiUser | null, @@ -51,7 +57,16 @@ export class AbuseUserReportEntityService { targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, me, { schema: 'UserDetailedNotMe', }), - assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { + // return hint, or pack by relation, or fetch and pack by id, or null + targetInstance: hint?.packedTargetInstance ?? ( + report.targetUserInstance + ? this.instanceEntityService.pack(report.targetUserInstance, me) + : report.targetUserHost + ? this.instancesRepository.findOneBy({ host: report.targetUserHost }).then(instance => instance + ? this.instanceEntityService.pack(instance, me) + : null) + : null), + assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, me, { schema: 'UserDetailedNotMe', }) : null, forwarded: report.forwarded, @@ -73,12 +88,18 @@ export class AbuseUserReportEntityService { me, { schema: 'UserDetailedNotMe' }, ).then(users => new Map(users.map(u => [u.id, u]))); + const _targetInstances = reports + .map(({ targetUserInstance, targetUserHost }) => targetUserInstance ?? targetUserHost) + .filter((i): i is MiInstance | string => i != null); + const _instanceMap = await this.instanceEntityService.packMany(await this.instanceEntityService.fetchInstancesByHost(_targetInstances), me) + .then(instances => new Map(instances.map(i => [i.host, i]))); return Promise.all( reports.map(report => { const packedReporter = _userMap.get(report.reporterId); const packedTargetUser = _userMap.get(report.targetUserId); + const packedTargetInstance = report.targetUserHost ? _instanceMap.get(report.targetUserHost) : undefined; const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined; - return this.pack(report, { packedReporter, packedTargetUser, packedAssignee }, me); + return this.pack(report, { packedReporter, packedTargetUser, packedAssignee, packedTargetInstance }, me); }), ); } diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index d43ebf9342..8f8d759004 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -4,6 +4,7 @@ */ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { MiInstance } from '@/models/Instance.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; @@ -88,11 +89,31 @@ export class MiAbuseUserReport { }) public targetUserHost: string | null; + @ManyToOne(() => MiInstance, { + // TODO create a foreign key constraint after hazelnoot/labs/persisted-instance-blocks is merged + createForeignKeyConstraints: false, + }) + @JoinColumn({ + name: 'targetUserHost', + referencedColumnName: 'host', + }) + public targetUserInstance: MiInstance | null; + @Index() @Column('varchar', { length: 128, nullable: true, comment: '[Denormalized]', }) public reporterHost: string | null; + + @ManyToOne(() => MiInstance, { + // TODO create a foreign key constraint after hazelnoot/labs/persisted-instance-blocks is merged + createForeignKeyConstraints: false, + }) + @JoinColumn({ + name: 'reporterHost', + referencedColumnName: 'host', + }) + public reporterInstance: MiInstance | null; //#endregion } diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index ad0bbb5647..f052d3dd14 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -69,6 +69,11 @@ export const meta = { nullable: false, optional: false, ref: 'UserDetailedNotMe', }, + targetInstance: { + type: 'object', + nullable: true, optional: false, + ref: 'FederationInstance', + }, assignee: { type: 'object', nullable: true, optional: false, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5e91fb14ac..b1d5f63a8b 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6198,6 +6198,7 @@ export type operations = { assigneeId: string | null; reporter: components['schemas']['UserDetailedNotMe']; targetUser: components['schemas']['UserDetailedNotMe']; + targetInstance: components['schemas']['FederationInstance'] | null; assignee: components['schemas']['UserDetailedNotMe'] | null; forwarded: boolean; /** @enum {string|null} */ From 23302fe7d8de4923f4748f6c64d39e3f57c9d98f Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:02:27 -0400 Subject: [PATCH 04/31] add relation from user->user_profile to speed up UserEntityService.pack and packMany --- .../src/core/entities/UserEntityService.ts | 34 +++++++++++++++---- packages/backend/src/models/User.ts | 4 +++ packages/backend/src/models/UserProfile.ts | 2 +- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 3524119ba1..326baaefd4 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -487,7 +487,10 @@ export class UserEntityService implements OnModuleInit { includeSecrets: false, }, options); - const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); + const user = typeof src === 'object' ? src : await this.usersRepository.findOneOrFail({ + where: { id: src }, + relations: { userProfile: true }, + }); // migration if (user.avatarId != null && user.avatarUrl === null) { @@ -521,7 +524,7 @@ export class UserEntityService implements OnModuleInit { const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const profile = isDetailed - ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) + ? (opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; let relation: UserRelation | null = null; @@ -556,7 +559,7 @@ export class UserEntityService implements OnModuleInit { } } - const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; + const mastoapi = !isDetailed ? opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; const followingCount = profile == null ? null : (profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount : @@ -785,8 +788,13 @@ export class UserEntityService implements OnModuleInit { const _users = users.filter((user): user is MiUser => typeof user !== 'string'); if (_users.length !== users.length) { _users.push( - ...await this.usersRepository.findBy({ - id: In(users.filter((user): user is string => typeof user === 'string')), + ...await this.usersRepository.find({ + where: { + id: In(users.filter((user): user is string => typeof user === 'string')), + }, + relations: { + userProfile: true, + }, }), ); } @@ -800,8 +808,20 @@ export class UserEntityService implements OnModuleInit { let pinNotes: Map = new Map(); if (options?.schema !== 'UserLite') { - profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) - .then(profiles => new Map(profiles.map(p => [p.userId, p]))); + const _profiles: MiUserProfile[] = []; + const _profilesToFetch: string[] = []; + for (const user of _users) { + if (user.userProfile) { + _profiles.push(user.userProfile); + } else { + _profilesToFetch.push(user.id); + } + } + if (_profilesToFetch.length > 0) { + const fetched = await this.userProfilesRepository.findBy({ userId: In(_profilesToFetch) }); + _profiles.push(...fetched); + } + profilesMap = new Map(_profiles.map(p => [p.userId, p])); const meId = me ? me.id : null; if (meId) { diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 3ef5817672..2f13400944 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -8,6 +8,7 @@ import { type UserUnsignedFetchOption, userUnsignedFetchOptions } from '@/const. import { MiInstance } from '@/models/Instance.js'; import { id } from './util/id.js'; import { MiDriveFile } from './DriveFile.js'; +import type { MiUserProfile } from './UserProfile.js'; @Entity('user') @Index(['usernameLower', 'host'], { unique: true }) @@ -395,6 +396,9 @@ export class MiUser { }) public attributionDomains: string[]; + @OneToOne('user_profile', (profile: MiUserProfile) => profile.user) + public userProfile: MiUserProfile | null; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 29c453dd71..6ee72e6ddd 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -17,7 +17,7 @@ export class MiUserProfile { @PrimaryColumn(id()) public userId: MiUser['id']; - @OneToOne(type => MiUser, { + @OneToOne(() => MiUser, user => user.userProfile, { onDelete: 'CASCADE', }) @JoinColumn() From b05ccbc3aca8e008555736f25760f9f2b1b2a5e7 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:03:05 -0400 Subject: [PATCH 05/31] add relations from abuse_user_report->user_profile to speed up admin/abuse-user-reports endpoint --- .../backend/src/models/AbuseUserReport.ts | 22 +++++++++++++++++++ .../api/endpoints/admin/abuse-user-reports.ts | 10 ++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index 8f8d759004..c1a44c3d40 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -5,6 +5,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import { MiInstance } from '@/models/Instance.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; @@ -25,6 +26,13 @@ export class MiAbuseUserReport { @JoinColumn() public targetUser: MiUser | null; + @ManyToOne(() => MiUserProfile, { + onDelete: 'CASCADE', + createForeignKeyConstraints: false, + }) + @JoinColumn({ name: 'targetUserId', referencedColumnName: 'userId' }) + public targetUserProfile: MiUserProfile | null; + @Index() @Column(id()) public reporterId: MiUser['id']; @@ -35,6 +43,13 @@ export class MiAbuseUserReport { @JoinColumn() public reporter: MiUser | null; + @ManyToOne(() => MiUserProfile, { + onDelete: 'CASCADE', + createForeignKeyConstraints: false, + }) + @JoinColumn({ name: 'reporterId', referencedColumnName: 'userId' }) + public reporterProfile: MiUserProfile | null; + @Column({ ...id(), nullable: true, @@ -47,6 +62,13 @@ export class MiAbuseUserReport { @JoinColumn() public assignee: MiUser | null; + @ManyToOne(() => MiUserProfile, { + onDelete: 'CASCADE', + createForeignKeyConstraints: false, + }) + @JoinColumn({ name: 'assigneeId', referencedColumnName: 'userId' }) + public assigneeProfile: MiUserProfile | null; + @Index() @Column('boolean', { default: false, diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index f052d3dd14..f9aa3b7ae5 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -120,7 +120,15 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId) + .leftJoinAndSelect('report.targetUser', 'targetUser') + .leftJoinAndSelect('report.targetUserProfile', 'targetUserProfile') + .leftJoinAndSelect('report.targetUserInstance', 'targetUserInstance') + .leftJoinAndSelect('report.reporter', 'reporter') + .leftJoinAndSelect('report.reporterUserProfile', 'reporterUserProfile') + .leftJoinAndSelect('report.assignee', 'assignee') + .leftJoinAndSelect('report.assigneeUserProfile', 'assigneeUserProfile') + ; switch (ps.state) { case 'resolved': query.andWhere('report.resolved = TRUE'); break; From c18edd106b8f0fef560d354a08f971555410c44f Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:04:08 -0400 Subject: [PATCH 06/31] implement de-duplication for MkUrlPreview --- .../frontend/src/components/MkUrlPreview.vue | 93 +++++++++++++++++-- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 69a1540600..eb4a4efa33 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only -->