enhance(profile): 相互リンク機能の改修 (MisskeyIO#684)

This commit is contained in:
まっちゃてぃー。 2024-08-11 08:32:14 +09:00 committed by GitHub
parent b6a5a36eaa
commit 5a9d8a5564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 471 additions and 900 deletions

View file

@ -5,8 +5,6 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@ -37,8 +35,6 @@ import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js';
import { UserBannerPiningService } from './UserBannerPiningService.js';
import { UserBannerService } from './UserBannerService.js';
import { NoteReadService } from './NoteReadService.js';
import { NotificationService } from './NotificationService.js';
import { PollService } from './PollService.js';
@ -177,8 +173,6 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $UserBannerPiningService: Provider = { provide: 'UserBannerPiningService', useExisting: UserBannerPiningService };
const $UserBannerService: Provider = { provide: 'UserBannerService', useExisting: UserBannerService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
@ -259,8 +253,6 @@ const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', use
const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService };
const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService };
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
const $UserBannerEntityService: Provider = { provide: 'UserBannerEntityService', useExisting: UserBannerEntityService };
const $UserBannerPiningEntityService: Provider = { provide: 'UserBannerPiningEntityService', useExisting: UserBannerPiningEntityService };
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
@ -323,8 +315,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
UserBannerPiningService,
UserBannerService,
NoteReadService,
NotificationService,
PollService,
@ -403,8 +393,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SigninEntityService,
UserEntityService,
UserListEntityService,
UserBannerEntityService,
UserBannerPiningEntityService,
FlashEntityService,
FlashLikeEntityService,
RoleEntityService,
@ -463,8 +451,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$UserBannerService,
$UserBannerPiningService,
$NoteReadService,
$NotificationService,
$PollService,
@ -543,8 +529,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SigninEntityService,
$UserEntityService,
$UserListEntityService,
$UserBannerEntityService,
$UserBannerPiningEntityService,
$FlashEntityService,
$FlashLikeEntityService,
$RoleEntityService,
@ -604,8 +588,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
UserBannerService,
UserBannerPiningService,
NoteReadService,
NotificationService,
PollService,
@ -683,8 +665,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SigninEntityService,
UserEntityService,
UserListEntityService,
UserBannerEntityService,
UserBannerPiningEntityService,
FlashEntityService,
FlashLikeEntityService,
RoleEntityService,
@ -743,8 +723,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$UserBannerService,
$UserBannerPiningService,
$NoteReadService,
$NotificationService,
$PollService,
@ -822,8 +800,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SigninEntityService,
$UserEntityService,
$UserListEntityService,
$UserBannerEntityService,
$UserBannerPiningEntityService,
$FlashEntityService,
$FlashLikeEntityService,
$RoleEntityService,

View file

@ -69,6 +69,8 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
mutualLinkSectionLimit: number;
mutualLinkLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -108,6 +110,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
mutualLinkSectionLimit: 1,
mutualLinkLimit: 15,
};
@Injectable()
@ -420,6 +424,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
mutualLinkSectionLimit: calc('mutualLinkSectionLimit', vs => Math.max(...vs)),
mutualLinkLimit: calc('mutualLinkLimit', vs => Math.max(...vs)),
};
}

View file

@ -1,55 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { MiUser } from '@/models/User.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import type { MiUserBannerPining, UserBannerPiningRepository, UserBannerRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class UserBannerPiningService {
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.userBannerPiningRepository)
private userBannerPiningRepository: UserBannerPiningRepository,
private idService: IdService,
) {
}
/**
*
* @param userId
* @param bannerIds
*/
public async addPinned(userId: MiUser['id'], bannerIds: MiUserBanner['id'][]) {
const pinsToInsert = bannerIds.map(bannerId => ({
id: this.idService.gen(),
userId,
pinnedBannerId: bannerId,
} as MiUserBannerPining));
await this.userBannerPiningRepository
.createQueryBuilder()
.insert()
.values(pinsToInsert)
.orIgnore()
.execute();
}
/**
*
* @param userId
* @param bannerIds
*/
@bindThis
public async removePinned(userId:MiUser['id'], bannerIds:MiUserBanner['id'][]) {
await this.userBannerPiningRepository.delete({
userId,
pinnedBannerId: In(bannerIds),
});
}
}

View file

@ -1,115 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { MiUser } from '@/models/User.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import type { DriveFilesRepository, MiDriveFile, UserBannerRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class UserBannerService {
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private idService: IdService,
) {
}
/**
*
* @param userId
* @param description
* @param url
* @param fileId
*/
@bindThis
public async create(userId: MiUser['id'], description: string | null, url: string, fileId: MiDriveFile['id']) {
const banner = await this.userBannerRepository.findOneBy({
userId,
});
if (banner) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'Already exists.');
const file = await this.driveFilesRepository.findOneBy({
id: fileId,
});
if (file == null) throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
return await this.userBannerRepository.insert({
id: this.idService.gen(),
userId,
description: description ?? null,
fileId: file.id,
url: url,
} as MiUserBanner);
}
/**
*
* @param userId
* @param bannerId
* @param description
* @param url
* @param fileId
*/
@bindThis
public async update(userId: MiUser['id'], bannerId: MiUserBanner['id'], description: string | null, url: string | null, fileId: MiDriveFile['id'] ) {
const banner = await this.userBannerRepository.findOneBy({
id: bannerId,
});
if (banner == null) {
throw new IdentifiableError('ac26da32-1659-4fbb-82c2-fc11a494799f', 'No such banner.');
}
if (banner.userId !== userId) {
throw new IdentifiableError('dfe79730-96f7-4d65-8c2a-b0975bf3524c', 'Not this user banner.');
}
const file = await this.driveFilesRepository.findOneBy({
id: fileId,
});
if (file == null) {
throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
}
await this.userBannerRepository.update({
id: bannerId,
}, {
description: description ?? null,
fileId: file.id,
url: url ?? null,
});
}
/**
*
* @param userId
* @param bannerId
*/
@bindThis
public async delete(userId: MiUser['id'], bannerId: MiUserBanner['id']) {
const banner = await this.userBannerRepository.findOneBy({
id: bannerId,
});
if (banner == null) {
throw new IdentifiableError('f4b158a5-610f-4ed3-b228-3507ebe1bba6', 'No such banner.');
}
if (banner.userId !== userId) {
throw new IdentifiableError('ad84053d-0cf4-4446-ac72-209adef15835', 'Not this user banner.');
}
await this.userBannerRepository.delete({
id: bannerId,
});
}
}

View file

@ -1,56 +0,0 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { bindThis } from '@/decorators.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, MiUserBanner, UserBannerRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Packed } from '@/misc/json-schema.js';
@Injectable()
export class UserBannerEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.userEntityService = this.moduleRef.get(UserEntityService.name);
}
@bindThis
public async pack(
src: MiUserBanner | MiUserBanner['id'] | null | undefined,
me: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'UserBanner'>> {
if (!src) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'No such banner.');
const banner = typeof src === 'object' ? src : await this.userBannerRepository.findOneByOrFail({ id: src });
const file = await this.driveFilesRepository.findOneByOrFail({ id: banner.fileId });
return {
id: banner.id,
user: await this.userEntityService.pack(banner.userId, me),
description: banner.description,
imgUrl: file.url,
url: banner.url,
fileId: file.id,
};
}
@bindThis
public async packMany(
src: MiUserBanner[] | MiUserBanner['id'][],
me: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'UserBanner'>[]> {
return (await Promise.allSettled(src.map(x => this.pack(x, me))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
}
}

View file

@ -1,23 +0,0 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserBannerPining } from '@/models/_.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { Packed } from '@/misc/json-schema.js';
@Injectable()
export class UserBannerPiningEntityService {
constructor(
private userBannerEntityService: UserBannerEntityService,
) {}
@bindThis
public async packMany(
src: MiUserBannerPining[],
me: { id: MiUser['id'] } | null | undefined,
) : Promise<Packed<'UserBanner'>[]> {
return (await Promise.allSettled(src.map(pining => this.userBannerEntityService.pack(pining.pinnedBannerId, me))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
}
}

View file

@ -28,20 +28,16 @@ import type {
FollowingsRepository,
FollowRequestsRepository,
MiFollowing,
MiUserBanner,
MiUserNotePining,
MiUserProfile,
MutingsRepository,
NoteUnreadsRepository,
RenoteMutingsRepository,
UserBannerRepository,
UserBannerPiningRepository,
UserMemoRepository,
UserNotePiningsRepository,
UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository,
MiUserBannerPining,
} from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@ -53,8 +49,6 @@ import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
@ -135,19 +129,11 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.userBannerPiningRepository)
private userBannerPiningRepository: UserBannerPiningRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository,
private userBannerEntityService: UserBannerEntityService,
private userBannerPiningEntityService: UserBannerPiningEntityService,
) {
}
@ -457,8 +443,6 @@ export class UserEntityService implements OnModuleInit {
}
let pins: MiUserNotePining[] = [];
let myMutualBanner: MiUserBanner | null = null;
let mutualBanners: MiUserBannerPining[] = [];
if (isDetailed) {
if (opts.pinNotes) {
pins = opts.pinNotes.get(user.id) ?? [];
@ -469,12 +453,6 @@ export class UserEntityService implements OnModuleInit {
.orderBy('pin.id', 'DESC')
.getMany();
}
if (user.id) {
[myMutualBanner, mutualBanners] = await Promise.all([
this.userBannerRepository.findOneBy({ userId: user.id }),
this.userBannerPiningRepository.findBy({ userId: user.id }),
]);
}
}
const followingCount = profile == null ? null :
@ -555,8 +533,7 @@ export class UserEntityService implements OnModuleInit {
lang: profile!.lang,
fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks,
mutualBanners: mutualBanners.length > 0 ? this.userBannerPiningEntityService.packMany(mutualBanners, me) : [],
myMutualBanner: myMutualBanner ? this.userBannerEntityService.pack(myMutualBanner, me) : null,
mutualLinkSections: profile!.mutualLinkSections,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,
@ -727,7 +704,7 @@ export class UserEntityService implements OnModuleInit {
}
}
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
}