なんかもうめっちゃ変えた
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
}
|
47
packages/backend/src/core/entities/AntennaEntityService.ts
Normal file
47
packages/backend/src/core/entities/AntennaEntityService.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
52
packages/backend/src/core/entities/AppEntityService.ts
Normal file
52
packages/backend/src/core/entities/AppEntityService.ts
Normal 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),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
42
packages/backend/src/core/entities/BlockingEntityService.ts
Normal file
42
packages/backend/src/core/entities/BlockingEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
66
packages/backend/src/core/entities/ChannelEntityService.ts
Normal file
66
packages/backend/src/core/entities/ChannelEntityService.ts
Normal 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,
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
43
packages/backend/src/core/entities/ClipEntityService.ts
Normal file
43
packages/backend/src/core/entities/ClipEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
214
packages/backend/src/core/entities/DriveFileEntityService.ts
Normal file
214
packages/backend/src/core/entities/DriveFileEntityService.ts
Normal 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);
|
||||
}
|
||||
}
|
@@ -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,
|
||||
}),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
43
packages/backend/src/core/entities/EmojiEntityService.ts
Normal file
43
packages/backend/src/core/entities/EmojiEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
98
packages/backend/src/core/entities/FollowingEntityService.ts
Normal file
98
packages/backend/src/core/entities/FollowingEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
41
packages/backend/src/core/entities/HashtagEntityService.ts
Normal file
41
packages/backend/src/core/entities/HashtagEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
59
packages/backend/src/core/entities/InstanceEntityService.ts
Normal file
59
packages/backend/src/core/entities/InstanceEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
45
packages/backend/src/core/entities/MutingEntityService.ts
Normal file
45
packages/backend/src/core/entities/MutingEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
396
packages/backend/src/core/entities/NoteEntityService.ts
Normal file
396
packages/backend/src/core/entities/NoteEntityService.ts
Normal 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();
|
||||
}
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
}
|
@@ -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),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
151
packages/backend/src/core/entities/NotificationEntityService.ts
Normal file
151
packages/backend/src/core/entities/NotificationEntityService.ts
Normal 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,
|
||||
},
|
||||
})));
|
||||
}
|
||||
}
|
109
packages/backend/src/core/entities/PageEntityService.ts
Normal file
109
packages/backend/src/core/entities/PageEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
41
packages/backend/src/core/entities/PageLikeEntityService.ts
Normal file
41
packages/backend/src/core/entities/PageLikeEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
27
packages/backend/src/core/entities/SigninEntityService.ts
Normal file
27
packages/backend/src/core/entities/SigninEntityService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
515
packages/backend/src/core/entities/UserEntityService.ts
Normal file
515
packages/backend/src/core/entities/UserEntityService.ts
Normal 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)));
|
||||
}
|
||||
}
|
42
packages/backend/src/core/entities/UserGroupEntityService.ts
Normal file
42
packages/backend/src/core/entities/UserGroupEntityService.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
41
packages/backend/src/core/entities/UserListEntityService.ts
Normal file
41
packages/backend/src/core/entities/UserListEntityService.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user