なんかもうめっちゃ変えた

This commit is contained in:
syuilo
2022-09-18 03:27:08 +09:00
committed by GitHub
parent d9ab03f086
commit b75184ec8e
946 changed files with 41219 additions and 28839 deletions

View File

@@ -0,0 +1,49 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { AbuseUserReportsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class AbuseUserReportEntityService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: AbuseUserReport['id'] | AbuseUserReport,
) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: report.id,
createdAt: report.createdAt.toISOString(),
comment: report.comment,
resolved: report.resolved,
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
detail: true,
}),
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
detail: true,
}),
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
detail: true,
}) : null,
forwarded: report.forwarded,
});
}
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,47 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { Antenna } from '@/models/entities/Antenna.js';
@Injectable()
export class AntennaEntityService {
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
) {
}
public async pack(
src: Antenna['id'] | Antenna,
): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await this.userGroupJoiningsRepository.findOneBy({ id: antenna.userGroupJoiningId }) : null;
return {
id: antenna.id,
createdAt: antenna.createdAt.toISOString(),
name: antenna.name,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
src: antenna.src,
userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users,
caseSensitive: antenna.caseSensitive,
notify: antenna.notify,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
hasUnreadNote,
};
}
}

View File

@@ -0,0 +1,52 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { AccessTokensRepository, AppsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { App } from '@/models/entities/App.js';
import type { User } from '@/models/entities/User.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class AppEntityService {
constructor(
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
) {
}
public async pack(
src: App['id'] | App,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean,
includeSecret?: boolean,
includeProfileImageIds?: boolean
},
): Promise<Packed<'App'>> {
const opts = Object.assign({
detail: false,
includeSecret: false,
includeProfileImageIds: false,
}, options);
const app = typeof src === 'object' ? src : await this.appsRepository.findOneByOrFail({ id: src });
return {
id: app.id,
name: app.name,
callbackUrl: app.callbackUrl,
permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}),
...(me ? {
isAuthorized: await this.accessTokensRepository.countBy({
appId: app.id,
userId: me.id,
}).then(count => count > 0),
} : {}),
};
}
}

View File

@@ -0,0 +1,33 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { AuthSessionsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { AuthSession } from '@/models/entities/AuthSession.js';
import type { User } from '@/models/entities/User.js';
import { UserEntityService } from './UserEntityService.js';
import { AppEntityService } from './AppEntityService.js';
@Injectable()
export class AuthSessionEntityService {
constructor(
@Inject(DI.authSessionsRepository)
private authSessionsRepository: AuthSessionsRepository,
private appEntityService: AppEntityService,
) {
}
public async pack(
src: AuthSession['id'] | AuthSession,
me?: { id: User['id'] } | null | undefined,
) {
const session = typeof src === 'object' ? src : await this.authSessionsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: session.id,
app: this.appEntityService.pack(session.appId, me),
token: session.token,
});
}
}

View File

@@ -0,0 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { BlockingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { Blocking } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class BlockingEntityService {
constructor(
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Blocking['id'] | Blocking,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: blocking.id,
createdAt: blocking.createdAt.toISOString(),
blockeeId: blocking.blockeeId,
blockee: this.userEntityService.pack(blocking.blockeeId, me, {
detail: true,
}),
});
}
public packMany(
blockings: any[],
me: { id: User['id'] },
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,66 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@Injectable()
export class ChannelEntityService {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
public async pack(
src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null;
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
const following = meId ? await this.channelFollowingsRepository.findOneBy({
followerId: meId,
followeeId: channel.id,
}) : null;
return {
id: channel.id,
createdAt: channel.createdAt.toISOString(),
lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
name: channel.name,
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
...(me ? {
isFollowing: following != null,
hasUnreadNote,
} : {}),
};
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { ClipsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Clip } from '@/models/entities/Clip.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class ClipEntityService {
constructor(
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Clip['id'] | Clip,
): Promise<Packed<'Clip'>> {
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: clip.id,
createdAt: clip.createdAt.toISOString(),
userId: clip.userId,
user: this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
});
}
public packMany(
clips: Clip[],
) {
return Promise.all(clips.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,214 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js';
import { NotesRepository, DriveFilesRepository } from '@/models/index.js';
import { Config } from '@/config.js';
import type { Packed } from '@/misc/schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { User } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
import { UtilityService } from '../UtilityService.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
type PackOptions = {
detail?: boolean,
self?: boolean,
withUser?: boolean,
};
@Injectable()
export class DriveFileEntityService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
// 循環参照のため / for circular dependency
@Inject(forwardRef(() => UserEntityService))
private userEntityService: UserEntityService,
private utilityService: UtilityService,
private driveFolderEntityService: DriveFolderEntityService,
) {
}
public validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200) &&
(name.indexOf('\\') === -1) &&
(name.indexOf('/') === -1) &&
(name.indexOf('..') === -1)
);
}
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
// TODO
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
}
public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) {
return appendQuery(this.config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? '1' : undefined,
}));
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
return `${this.config.url}/files/${key}`;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url);
}
public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
const id = typeof user === 'object' ? user.id : user;
const { sum } = await this.driveFilesRepository
.createQueryBuilder('file')
.where('file.userId = :id', { id: id })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
}
public async calcDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this.driveFilesRepository
.createQueryBuilder('file')
.where('file.userHost = :host', { host: this.utilityService.toPuny(host) })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
}
public async calcDriveUsageOfLocal(): Promise<number> {
const { sum } = await this.driveFilesRepository
.createQueryBuilder('file')
.where('file.userHost IS NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
}
public async calcDriveUsageOfRemote(): Promise<number> {
const { sum } = await this.driveFilesRepository
.createQueryBuilder('file')
.where('file.userHost IS NOT NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
}
public async pack(
src: DriveFile['id'] | DriveFile,
options?: PackOptions,
): Promise<Packed<'DriveFile'>> {
const opts = Object.assign({
detail: false,
self: false,
}, options);
const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneByOrFail({ id: src });
return await awaitAll<Packed<'DriveFile'>>({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
});
}
public async packNullable(
src: DriveFile['id'] | DriveFile,
options?: PackOptions,
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
self: false,
}, options);
const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneBy({ id: src });
if (file == null) return null;
return await awaitAll<Packed<'DriveFile'>>({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
});
}
public async packMany(
files: (DriveFile['id'] | DriveFile)[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
return items.filter((x): x is Packed<'DriveFile'> => x != null);
}
}

View File

@@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { DriveFolder } from '@/models/entities/DriveFolder.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class DriveFolderEntityService {
constructor(
@Inject(DI.driveFoldersRepository)
private driveFoldersRepository: DriveFoldersRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
) {
}
public async pack(
src: DriveFolder['id'] | DriveFolder,
options?: {
detail: boolean
},
): Promise<Packed<'DriveFolder'>> {
const opts = Object.assign({
detail: false,
}, options);
const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: folder.id,
createdAt: folder.createdAt.toISOString(),
name: folder.name,
parentId: folder.parentId,
...(opts.detail ? {
foldersCount: this.driveFoldersRepository.countBy({
parentId: folder.id,
}),
filesCount: this.driveFilesRepository.countBy({
folderId: folder.id,
}),
...(folder.parentId ? {
parent: this.pack(folder.parentId, {
detail: true,
}),
} : {}),
} : {}),
});
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { EmojisRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class EmojiEntityService {
constructor(
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Emoji['id'] | Emoji,
): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
// ?? emoji.originalUrl してるのは後方互換性のため
url: emoji.publicUrl ?? emoji.originalUrl,
};
}
public packMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,34 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { FollowRequestsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { FollowRequest } from '@/models/entities/FollowRequest.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class FollowRequestEntityService {
constructor(
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: FollowRequest['id'] | FollowRequest,
me?: { id: User['id'] } | null | undefined,
) {
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
return {
id: request.id,
follower: await this.userEntityService.pack(request.followerId, me),
followee: await this.userEntityService.pack(request.followeeId, me),
};
}
}

View File

@@ -0,0 +1,98 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { FollowingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Following } from '@/models/entities/Following.js';
import { UserEntityService } from './UserEntityService.js';
type LocalFollowerFollowing = Following & {
followerHost: null;
followerInbox: null;
followerSharedInbox: null;
};
type RemoteFollowerFollowing = Following & {
followerHost: string;
followerInbox: string;
followerSharedInbox: string;
};
type LocalFolloweeFollowing = Following & {
followeeHost: null;
followeeInbox: null;
followeeSharedInbox: null;
};
type RemoteFolloweeFollowing = Following & {
followeeHost: string;
followeeInbox: string;
followeeSharedInbox: string;
};
@Injectable()
export class FollowingEntityService {
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
) {
}
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
return following.followerHost == null;
}
public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
return following.followerHost != null;
}
public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
return following.followeeHost == null;
}
public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
return following.followeeHost != null;
}
public async pack(
src: Following['id'] | Following,
me?: { id: User['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
if (opts == null) opts = {};
return await awaitAll({
id: following.id,
createdAt: following.createdAt.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
detail: true,
}) : undefined,
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
detail: true,
}) : undefined,
});
}
public packMany(
followings: any[],
me?: { id: User['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
}
}

View File

@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { GalleryPosts, GalleryLikesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { GalleryLike } from '@/models/entities/GalleryLike.js';
import { UserEntityService } from './UserEntityService.js';
import { GalleryPostEntityService } from './GalleryPostEntityService.js';
@Injectable()
export class GalleryLikeEntityService {
constructor(
@Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository,
private galleryPostEntityService: GalleryPostEntityService,
) {
}
public async pack(
src: GalleryLike['id'] | GalleryLike,
me?: any,
) {
const like = typeof src === 'object' ? src : await this.galleryLikesRepository.findOneByOrFail({ id: src });
return {
id: like.id,
post: await this.galleryPostEntityService.pack(like.post ?? like.postId, me),
};
}
public packMany(
likes: any[],
me: any,
) {
return Promise.all(likes.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { GalleryPost } from '@/models/entities/GalleryPost.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@Injectable()
export class GalleryPostEntityService {
constructor(
@Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository,
@Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
public async pack(
src: GalleryPost['id'] | GalleryPost,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: post.id,
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
user: this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
files: this.driveFileEntityService.packMany(post.fileIds),
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
});
}
public packMany(
posts: GalleryPost[],
me?: { id: User['id'] } | null | undefined,
) {
return Promise.all(posts.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { HashtagsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Hashtag } from '@/models/entities/Hashtag.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class HashtagEntityService {
constructor(
@Inject(DI.hashtagsRepository)
private hashtagsRepository: HashtagsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Hashtag,
): Promise<Packed<'Hashtag'>> {
return {
tag: src.name,
mentionedUsersCount: src.mentionedUsersCount,
mentionedLocalUsersCount: src.mentionedLocalUsersCount,
mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
attachedUsersCount: src.attachedUsersCount,
attachedLocalUsersCount: src.attachedLocalUsersCount,
attachedRemoteUsersCount: src.attachedRemoteUsersCount,
};
}
public packMany(
hashtags: Hashtag[],
) {
return Promise.all(hashtags.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,59 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { InstancesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Instance } from '@/models/entities/Instance.js';
import { MetaService } from '../MetaService.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class InstanceEntityService {
constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private metaService: MetaService,
) {
}
public async pack(
instance: Instance,
): Promise<Packed<'FederationInstance'>> {
const meta = await this.metaService.fetch();
return {
id: instance.id,
caughtAt: instance.caughtAt.toISOString(),
host: instance.host,
usersCount: instance.usersCount,
notesCount: instance.notesCount,
followingCount: instance.followingCount,
followersCount: instance.followersCount,
latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null,
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isBlocked: meta.blockedHosts.includes(instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,
name: instance.name,
description: instance.description,
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
};
}
public packMany(
instances: Instance[],
) {
return Promise.all(instances.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MessagingMessagesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { UserGroupEntityService } from './UserGroupEntityService.js';
@Injectable()
export class MessagingMessageEntityService {
constructor(
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private userEntityService: UserEntityService,
private userGroupEntityService: UserGroupEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
public async pack(
src: MessagingMessage['id'] | MessagingMessage,
me?: { id: User['id'] } | null | undefined,
options?: {
populateRecipient?: boolean,
populateGroup?: boolean,
},
): Promise<Packed<'MessagingMessage'>> {
const opts = options ?? {
populateRecipient: true,
populateGroup: true,
};
const message = typeof src === 'object' ? src : await this.messagingMessagesRepository.findOneByOrFail({ id: src });
return {
id: message.id,
createdAt: message.createdAt.toISOString(),
text: message.text,
userId: message.userId,
user: await this.userEntityService.pack(message.user ?? message.userId, me),
recipientId: message.recipientId,
recipient: message.recipientId && opts.populateRecipient ? await this.userEntityService.pack(message.recipient ?? message.recipientId, me) : undefined,
groupId: message.groupId,
group: message.groupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.group ?? message.groupId) : undefined,
fileId: message.fileId,
file: message.fileId ? await this.driveFileEntityService.pack(message.fileId) : null,
isRead: message.isRead,
reads: message.reads,
};
}
}

View File

@@ -0,0 +1,44 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { ModerationLogsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { ModerationLog } from '@/models/entities/ModerationLog.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class ModerationLogEntityService {
constructor(
@Inject(DI.moderationLogsRepository)
private moderationLogsRepository: ModerationLogsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: ModerationLog['id'] | ModerationLog,
) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: log.id,
createdAt: log.createdAt.toISOString(),
type: log.type,
info: log.info,
userId: log.userId,
user: this.userEntityService.pack(log.user ?? log.userId, null, {
detail: true,
}),
});
}
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,45 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MutingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Muting } from '@/models/entities/Muting.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class MutingEntityService {
constructor(
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Muting['id'] | Muting,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: muting.id,
createdAt: muting.createdAt.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true,
}),
});
}
public packMany(
mutings: any[],
me: { id: User['id'] },
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,396 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import * as mfm from 'mfm-js';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { Notes, Polls, PollVotes, DriveFiles, Channels, Followings, Users, NoteReactions } from '@/models/index.js';
import { Config } from '@/config.js';
import type { Packed } from '@/misc/schema.js';
import { nyaize } from '@/misc/nyaize.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
@Injectable()
export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService;
private reactionService: ReactionService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
//private userEntityService: UserEntityService,
//private driveFileEntityService: DriveFileEntityService,
//private customEmojiService: CustomEmojiService,
//private reactionService: ReactionService,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService');
}
async #hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
if (specified) {
hide = false;
} else {
hide = true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
const following = await this.followingsRepository.findOneBy({
followeeId: packedNote.userId,
followerId: meId,
});
if (following == null) {
hide = true;
} else {
hide = false;
}
}
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
}
}
async #populatePoll(note: Note, meId: User['id'] | null) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
const choices = poll.choices.map(c => ({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false,
}));
if (meId) {
if (poll.multiple) {
const votes = await this.pollVotesRepository.findBy({
userId: meId,
noteId: note.id,
});
const myChoices = votes.map(v => v.choice);
for (const myChoice of myChoices) {
choices[myChoice].isVoted = true;
}
} else {
const vote = await this.pollVotesRepository.findOneBy({
userId: meId,
noteId: note.id,
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
}
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices,
};
}
async #populateMyReaction(note: Note, meId: User['id'], _hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
}) {
if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId,
noteId: note.id,
});
if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction);
}
return undefined;
}
public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
return note.visibleUserIds.some((id: any) => meId === id);
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
const [following, user] = await Promise.all([
this.followingsRepository.count({
where: {
followeeId: note.userId,
followerId: meId,
},
take: 1,
}),
this.usersRepository.findOneByOrFail({ id: meId }),
]);
/* If we know the following, everyhting is fine.
But if we do not know the following, it might be that both the
author of the note and the author of the like are remote users,
in which case we can never know the following. Instead we have
to assume that the users are following each other.
*/
return following > 0 || (note.userHost != null && user.host != null);
}
}
return true;
}
public async pack(
src: Note['id'] | Note,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
_hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
};
},
): Promise<Packed<'Note'>> {
const opts = Object.assign({
detail: true,
skipHide: false,
}, options);
const meId = me ? me.id : null;
const note = typeof src === 'object' ? src : await this.notesRepository.findOneByOrFail({ id: src });
const host = note.userHost;
let text = note.text;
if (note.name && (note.url ?? note.uri)) {
text = `${note.name}\n${(note.text || '').trim()}\n\n${note.url ?? note.uri}`;
}
const channel = note.channelId
? note.channel
? note.channel
: await this.channelsRepository.findOneBy({ id: note.channelId })
: null;
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x.startsWith(':')).map(x => this.reactionService.decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
detail: false,
}),
text: text,
cw: note.cw,
visibility: note.visibility,
localOnly: note.localOnly ?? undefined,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions),
tags: note.tags.length > 0 ? note.tags : undefined,
emojis: this.customEmojiService.populateEmojis(note.emojis.concat(reactionEmojiNames), host),
fileIds: note.fileIds,
files: this.driveFileEntityService.packMany(note.fileIds),
replyId: note.replyId,
renoteId: note.renoteId,
channelId: note.channelId ?? undefined,
channel: channel ? {
id: channel.id,
name: channel.name,
} : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined,
url: note.url ?? undefined,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false,
_hint_: options?._hint_,
}) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
detail: true,
_hint_: options?._hint_,
}) : undefined,
poll: note.hasPoll ? this.#populatePoll(note, meId) : undefined,
...(meId ? {
myReaction: this.#populateMyReaction(note, meId, options?._hint_),
} : {}),
} : {}),
});
if (packed.user.isCat && packed.text) {
const tokens = packed.text ? mfm.parse(packed.text) : [];
mfm.inspect(tokens, node => {
if (node.type === 'text') {
// TODO: quoteなtextはskip
node.props.text = nyaize(node.props.text);
}
});
packed.text = mfm.toString(tokens);
}
if (!opts.skipHide) {
await this.#hideNote(packed, meId);
}
return packed;
}
public async packMany(
notes: Note[],
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
},
) {
if (notes.length === 0) return [];
const meId = me ? me.id : null;
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...notes.map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
}
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
},
})));
}
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
const query = this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId });
// 指定した投稿を除く
if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
}
return await query.getCount();
}
}

View File

@@ -0,0 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { NoteFavoritesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { UserEntityService } from './UserEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';
@Injectable()
export class NoteFavoriteEntityService {
constructor(
@Inject(DI.noteFavoritesRepository)
private noteFavoritesRepository: NoteFavoritesRepository,
private noteEntityService: NoteEntityService,
) {
}
public async pack(
src: NoteFavorite['id'] | NoteFavorite,
me?: { id: User['id'] } | null | undefined,
) {
const favorite = typeof src === 'object' ? src : await this.noteFavoritesRepository.findOneByOrFail({ id: src });
return {
id: favorite.id,
createdAt: favorite.createdAt.toISOString(),
noteId: favorite.noteId,
note: await this.noteEntityService.pack(favorite.note ?? favorite.noteId, me),
};
}
public packMany(
favorites: any[],
me: { id: User['id'] },
) {
return Promise.all(favorites.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,62 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { NoteReactionsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { OnModuleInit } from '@nestjs/common';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class NoteReactionEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private reactionService: ReactionService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
//private reactionService: ReactionService,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.reactionService = this.moduleRef.get('ReactionService');
}
public async pack(
src: NoteReaction['id'] | NoteReaction,
me?: { id: User['id'] } | null | undefined,
options?: {
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
return {
id: reaction.id,
createdAt: reaction.createdAt.toISOString(),
user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
} : {}),
};
}
}

View File

@@ -0,0 +1,151 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Note } from '@/models/entities/Note.js';
import type { Packed } from '@/misc/schema.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js';
@Injectable()
export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private userGroupInvitationEntityService: UserGroupInvitationEntityService;
private customEmojiService: CustomEmojiService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
//private userGroupInvitationEntityService: UserGroupInvitationEntityService,
//private customEmojiService: CustomEmojiService,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
}
public async pack(
src: Notification['id'] | Notification,
options: {
_hintForEachNotes_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
};
},
): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src });
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
return await awaitAll({
id: notification.id,
createdAt: notification.createdAt.toISOString(),
type: notification.type,
isRead: notification.isRead,
userId: notification.notifierId,
user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
...(notification.type === 'mention' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'reply' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'renote' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'quote' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'reaction' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
reaction: notification.reaction,
} : {}),
...(notification.type === 'pollVote' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
choice: notification.choice,
} : {}),
...(notification.type === 'pollEnded' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader ?? token?.name,
icon: notification.customIcon ?? token?.iconUrl,
} : {}),
});
}
public async packMany(
notifications: Notification[],
meId: User['id'],
) {
if (notifications.length === 0) return [];
const notes = notifications.filter(x => x.note != null).map(x => x.note!);
const noteIds = notes.map(n => n.id);
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...noteIds, ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, {
_hintForEachNotes_: {
myReactions: myReactionsMap,
},
})));
}
}

View File

@@ -0,0 +1,109 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Page } from '@/models/entities/Page.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@Injectable()
export class PageEntityService {
constructor(
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@Inject(DI.pageLikesRepository)
private pageLikesRepository: PageLikesRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
public async pack(
src: Page['id'] | Page,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Page'>> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
const attachedFiles: Promise<DriveFile | null>[] = [];
const collectFile = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'image') {
attachedFiles.push(this.driveFilesRepository.findOneBy({
id: x.fileId,
userId: page.userId,
}));
}
if (x.children) {
collectFile(x.children);
}
}
};
collectFile(page.content);
// 後方互換性のため
let migrated = false;
const migrate = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'input') {
if (x.inputType === 'text') {
x.type = 'textInput';
}
if (x.inputType === 'number') {
x.type = 'numberInput';
if (x.default) x.default = parseInt(x.default, 10);
}
migrated = true;
}
if (x.children) {
migrate(x.children);
}
}
};
migrate(page.content);
if (migrated) {
this.pagesRepository.update(page.id, {
content: page.content,
});
}
return await awaitAll({
id: page.id,
createdAt: page.createdAt.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
name: page.name,
summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned,
alignCenter: page.alignCenter,
font: page.font,
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
});
}
public packMany(
pages: Page[],
me?: { id: User['id'] } | null | undefined,
) {
return Promise.all(pages.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { PageLikesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { PageLike } from '@/models/entities/PageLike.js';
import { UserEntityService } from './UserEntityService.js';
import { PageEntityService } from './PageEntityService.js';
@Injectable()
export class PageLikeEntityService {
constructor(
@Inject(DI.pageLikesRepository)
private pageLikesRepository: PageLikesRepository,
private pageEntityService: PageEntityService,
) {
}
public async pack(
src: PageLike['id'] | PageLike,
me?: { id: User['id'] } | null | undefined,
) {
const like = typeof src === 'object' ? src : await this.pageLikesRepository.findOneByOrFail({ id: src });
return {
id: like.id,
page: await this.pageEntityService.pack(like.page ?? like.pageId, me),
};
}
public packMany(
likes: any[],
me: { id: User['id'] },
) {
return Promise.all(likes.map(x => this.pack(x, me)));
}
}

View File

@@ -0,0 +1,27 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { SigninsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Signin } from '@/models/entities/Signin.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class SigninEntityService {
constructor(
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: Signin,
) {
return src;
}
}

View File

@@ -0,0 +1,515 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { EntityRepository, Repository, In, Not } from 'typeorm';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import { Config } from '@/config.js';
import type { Packed } from '@/misc/schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { OnModuleInit } from '@nestjs/common';
import type { AntennaService } from '../AntennaService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
Detailed extends true ?
ExpectsMe extends true ? Packed<'MeDetailed'> :
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
Packed<'UserDetailed'> :
Packed<'UserLite'>;
const ajv = new Ajv();
function isLocalUser(user: User): user is ILocalUser;
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
function isLocalUser(user: User | { host: User['host'] }): boolean {
return user.host == null;
}
function isRemoteUser(user: User): user is IRemoteUser;
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
function isRemoteUser(user: User | { host: User['host'] }): boolean {
return !isLocalUser(user);
}
@Injectable()
export class UserEntityService implements OnModuleInit {
private noteEntityService: NoteEntityService;
private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private antennaService: AntennaService;
#userInstanceCache: Cache<Instance | null>;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
//private noteEntityService: NoteEntityService,
//private driveFileEntityService: DriveFileEntityService,
//private pageEntityService: PageEntityService,
//private customEmojiService: CustomEmojiService,
//private antennaService: AntennaService,
) {
this.#userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
}
onModuleInit() {
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.antennaService = this.moduleRef.get('AntennaService');
}
//#region Validators
public validateLocalUsername = ajv.compile(localUsernameSchema);
public validatePassword = ajv.compile(passwordSchema);
public validateName = ajv.compile(nameSchema);
public validateDescription = ajv.compile(descriptionSchema);
public validateLocation = ajv.compile(locationSchema);
public validateBirthday = ajv.compile(birthdaySchema);
//#endregion
public isLocalUser = isLocalUser;
public isRemoteUser = isRemoteUser;
public async getRelation(me: User['id'], target: User['id']) {
return awaitAll({
id: target,
isFollowing: this.followingsRepository.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
isFollowed: this.followingsRepository.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestFromYou: this.followRequestsRepository.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestToYou: this.followRequestsRepository.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
isBlocking: this.blockingsRepository.count({
where: {
blockerId: me,
blockeeId: target,
},
take: 1,
}).then(n => n > 0),
isBlocked: this.blockingsRepository.count({
where: {
blockerId: target,
blockeeId: me,
},
take: 1,
}).then(n => n > 0),
isMuted: this.mutingsRepository.count({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
});
}
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
const joinings = await this.userGroupJoiningsRepository.findBy({ userId: userId });
const groupQs = Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder('message')
.where('message.groupId = :groupId', { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
this.messagingMessagesRepository.count({
where: {
recipientId: userId,
isRead: false,
...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
},
take: 1,
}).then(count => count > 0),
groupQs,
]);
return withUser || withGroups.some(x => x);
}
public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
const reads = await this.announcementReadsRepository.findBy({
userId: userId,
});
const count = await this.announcementsRepository.countBy(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId))),
} : {});
return count > 0;
}
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
antennaId: In(myAntennas.map(x => x.id)),
read: false,
}) : null;
return unread != null;
}
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
const channels = await this.channelFollowingsRepository.findBy({ followerId: userId });
const unread = channels.length > 0 ? await this.noteUnreadsRepository.findOneBy({
userId: userId,
noteChannelId: In(channels.map(x => x.followeeId)),
}) : null;
return unread != null;
}
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
const mutedUserIds = mute.map(m => m.muteeId);
const count = await this.notificationsRepository.count({
where: {
notifieeId: userId,
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
isRead: false,
},
take: 1,
});
return count > 0;
}
public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
const count = await this.followRequestsRepository.countBy({
followeeId: userId,
});
return count > 0;
}
public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
if (user.hideOnlineStatus) return 'unknown';
if (user.lastActiveDate == null) return 'unknown';
const elapsed = Date.now() - user.lastActiveDate.getTime();
return (
elapsed < USER_ONLINE_THRESHOLD ? 'online' :
elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
'offline'
);
}
public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
} else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
}
public getAvatarUrlSync(user: User): string {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
}
public getIdenticonUrl(userId: User['id']): string {
return `${this.config.url}/identicon/${userId}`;
}
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
src: User['id'] | User,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: D,
includeSecrets?: boolean,
},
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign({
detail: false,
includeSecrets: false,
}, options);
let user: User;
if (typeof src === 'object') {
user = src;
if (src.avatar === undefined && src.avatarId) src.avatar = await this.driveFilesRepository.findOneBy({ id: src.avatarId }) ?? null;
if (src.banner === undefined && src.bannerId) src.banner = await this.driveFilesRepository.findOneBy({ id: src.bannerId }) ?? null;
} else {
user = await this.usersRepository.findOneOrFail({
where: { id: src },
relations: {
avatar: true,
banner: true,
},
});
}
const meId = me ? me.id : null;
const isMe = meId === user.id;
const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany() : [];
const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const falsy = opts.detail ? false : undefined;
const packed = {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash ?? null,
avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin ?? falsy,
isModerator: user.isModerator ?? falsy,
isBot: user.isBot ?? falsy,
isCat: user.isCat ?? falsy,
instance: user.host ? this.#userInstanceCache.fetch(user.host,
() => this.instancesRepository.findOneBy({ host: user.host! }),
v => v != null,
).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
...(opts.detail ? {
url: profile!.url,
uri: user.uri,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null,
bannerBlurhash: user.banner?.blurhash ?? null,
bannerColor: null, // 後方互換性のため
isLocked: user.isLocked,
isSilenced: user.isSilenced ?? falsy,
isSuspended: user.isSuspended ?? falsy,
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,
pinnedNoteIds: pins.map(pin => pin.noteId),
pinnedNotes: this.noteEntityService.packMany(pins.map(pin => pin.note!), me, {
detail: true,
}),
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions,
ffVisibility: profile!.ffVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({
userId: user.id,
}).then(result => result >= 1)
: false,
} : {}),
...(opts.detail && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
autoSensitive: profile!.autoSensitive,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({
where: { userId: user.id, isSpecified: true },
take: 1,
}).then(count => count > 0),
hasUnreadMentions: this.noteUnreadsRepository.count({
where: { userId: user.id, isMentioned: true },
take: 1,
}).then(count => count > 0),
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: this.getHasUnreadChannel(user.id),
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies ?? falsy,
} : {}),
...(opts.includeSecrets ? {
email: profile!.email,
emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.find({
where: {
userId: user.id,
},
select: {
id: true,
name: true,
lastUsed: true,
},
})
: [],
} : {}),
...(relation ? {
isFollowing: relation.isFollowing,
isFollowed: relation.isFollowed,
hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
return await awaitAll(packed);
}
public packMany<D extends boolean = false>(
users: (User['id'] | User)[],
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: D,
includeSecrets?: boolean,
},
): Promise<IsUserDetailed<D>[]> {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
}

View File

@@ -0,0 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class UserGroupEntityService {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: UserGroup['id'] | UserGroup,
): Promise<Packed<'UserGroup'>> {
const userGroup = typeof src === 'object' ? src : await this.userGroupsRepository.findOneByOrFail({ id: src });
const users = await this.userGroupJoiningsRepository.findBy({
userGroupId: userGroup.id,
});
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name,
ownerId: userGroup.userId,
userIds: users.map(x => x.userId),
};
}
}

View File

@@ -0,0 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { UserGroupInvitationsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js';
import { UserEntityService } from './UserEntityService.js';
import { UserGroupEntityService } from './UserGroupEntityService.js';
@Injectable()
export class UserGroupInvitationEntityService {
constructor(
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
}
public async pack(
src: UserGroupInvitation['id'] | UserGroupInvitation,
) {
const invitation = typeof src === 'object' ? src : await this.userGroupInvitationsRepository.findOneByOrFail({ id: src });
return {
id: invitation.id,
group: await this.userGroupEntityService.pack(invitation.userGroup ?? invitation.userGroupId),
};
}
public packMany(
invitations: any[],
) {
return Promise.all(invitations.map(x => this.pack(x)));
}
}

View File

@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { UserList } from '@/models/entities/UserList.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class UserListEntityService {
constructor(
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
private userEntityService: UserEntityService,
) {
}
public async pack(
src: UserList['id'] | UserList,
): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
const users = await this.userListJoiningsRepository.findBy({
userListId: userList.id,
});
return {
id: userList.id,
createdAt: userList.createdAt.toISOString(),
name: userList.name,
userIds: users.map(x => x.userId),
};
}
}