feat(profile): 相互リンク機能の追加 (MisskeyIO#675)
This commit is contained in:
parent
b059162324
commit
b6a5a36eaa
37 changed files with 1130 additions and 20 deletions
|
@ -5,6 +5,8 @@
|
|||
|
||||
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';
|
||||
|
@ -35,6 +37,8 @@ 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';
|
||||
|
@ -173,6 +177,8 @@ 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 };
|
||||
|
@ -253,6 +259,8 @@ 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 };
|
||||
|
@ -315,6 +323,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
UserBannerPiningService,
|
||||
UserBannerService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
|
@ -393,6 +403,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
SigninEntityService,
|
||||
UserEntityService,
|
||||
UserListEntityService,
|
||||
UserBannerEntityService,
|
||||
UserBannerPiningEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
|
@ -451,6 +463,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$UserBannerService,
|
||||
$UserBannerPiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
|
@ -529,6 +543,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$SigninEntityService,
|
||||
$UserEntityService,
|
||||
$UserListEntityService,
|
||||
$UserBannerEntityService,
|
||||
$UserBannerPiningEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
|
@ -588,6 +604,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
UserBannerService,
|
||||
UserBannerPiningService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
|
@ -665,6 +683,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
SigninEntityService,
|
||||
UserEntityService,
|
||||
UserListEntityService,
|
||||
UserBannerEntityService,
|
||||
UserBannerPiningEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
|
@ -723,6 +743,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$UserBannerService,
|
||||
$UserBannerPiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
|
@ -800,6 +822,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$SigninEntityService,
|
||||
$UserEntityService,
|
||||
$UserListEntityService,
|
||||
$UserBannerEntityService,
|
||||
$UserBannerPiningEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
|
|
55
packages/backend/src/core/UserBannerPiningService.ts
Normal file
55
packages/backend/src/core/UserBannerPiningService.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
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),
|
||||
});
|
||||
}
|
||||
}
|
115
packages/backend/src/core/UserBannerService.ts
Normal file
115
packages/backend/src/core/UserBannerService.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -28,16 +28,20 @@ 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';
|
||||
|
@ -49,9 +53,10 @@ 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 { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
|
@ -130,11 +135,19 @@ 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,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -444,6 +457,8 @@ 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) ?? [];
|
||||
|
@ -454,6 +469,12 @@ 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 :
|
||||
|
@ -534,6 +555,8 @@ 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,
|
||||
followersCount: followersCount ?? 0,
|
||||
followingCount: followingCount ?? 0,
|
||||
notesCount: user.notesCount,
|
||||
|
@ -563,7 +586,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
displayOrder: role.displayOrder,
|
||||
}))
|
||||
})),
|
||||
),
|
||||
memo: memo,
|
||||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||
|
@ -704,7 +727,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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue