summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2026-03-05 10:56:50 +0000
committerGitHub <noreply@github.com>2026-03-05 10:56:50 +0000
commitfe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch)
treeaf6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/backend/src
parentMerge pull request #16998 from misskey-dev/develop (diff)
parentRelease: 2026.3.0 (diff)
downloadmisskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/boot/entry.ts12
-rw-r--r--packages/backend/src/boot/master.ts19
-rw-r--r--packages/backend/src/config.ts34
-rw-r--r--packages/backend/src/core/AccountMoveService.ts2
-rw-r--r--packages/backend/src/core/AnnouncementService.ts2
-rw-r--r--packages/backend/src/core/AvatarDecorationService.ts2
-rw-r--r--packages/backend/src/core/CoreModule.ts4
-rw-r--r--packages/backend/src/core/EmailService.ts2
-rw-r--r--packages/backend/src/core/FileInfoService.ts28
-rw-r--r--packages/backend/src/core/GlobalEventService.ts6
-rw-r--r--packages/backend/src/core/MfmService.ts8
-rw-r--r--packages/backend/src/core/NoteDraftService.ts4
-rw-r--r--packages/backend/src/core/QueueService.ts16
-rw-r--r--packages/backend/src/core/RoleService.ts2
-rw-r--r--packages/backend/src/core/SearchService.ts3
-rw-r--r--packages/backend/src/core/UserSuspendService.ts6
-rw-r--r--packages/backend/src/core/UtilityService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts14
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts79
-rw-r--r--packages/backend/src/core/activitypub/models/ApImageService.ts2
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts2
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts8
-rw-r--r--packages/backend/src/core/activitypub/models/ApQuestionService.ts4
-rw-r--r--packages/backend/src/core/entities/ChatEntityService.ts2
-rw-r--r--packages/backend/src/core/entities/DriveFileEntityService.ts45
-rw-r--r--packages/backend/src/core/entities/DriveFolderEntityService.ts154
-rw-r--r--packages/backend/src/core/entities/EmojiEntityService.ts4
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts4
-rw-r--r--packages/backend/src/core/entities/NoteReactionEntityService.ts4
-rw-r--r--packages/backend/src/core/entities/ReversiGameEntityService.ts8
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts2
-rw-r--r--packages/backend/src/misc/check-word-mute.ts2
-rw-r--r--packages/backend/src/misc/get-ip-hash.ts2
-rw-r--r--packages/backend/src/misc/i18n.ts2
-rw-r--r--packages/backend/src/misc/json-schema.ts4
-rw-r--r--packages/backend/src/misc/split-id-and-objects.ts27
-rw-r--r--packages/backend/src/misc/unique-by-key.ts21
-rw-r--r--packages/backend/src/models/AbuseReportNotificationRecipient.ts6
-rw-r--r--packages/backend/src/models/AbuseUserReport.ts6
-rw-r--r--packages/backend/src/models/AccessToken.ts4
-rw-r--r--packages/backend/src/models/Announcement.ts2
-rw-r--r--packages/backend/src/models/AnnouncementRead.ts4
-rw-r--r--packages/backend/src/models/Antenna.ts4
-rw-r--r--packages/backend/src/models/App.ts2
-rw-r--r--packages/backend/src/models/AuthSession.ts4
-rw-r--r--packages/backend/src/models/Blocking.ts4
-rw-r--r--packages/backend/src/models/BubbleGameRecord.ts2
-rw-r--r--packages/backend/src/models/Channel.ts4
-rw-r--r--packages/backend/src/models/ChannelFavorite.ts4
-rw-r--r--packages/backend/src/models/ChannelFollowing.ts4
-rw-r--r--packages/backend/src/models/ChannelMuting.ts4
-rw-r--r--packages/backend/src/models/ChatApproval.ts4
-rw-r--r--packages/backend/src/models/ChatMessage.ts8
-rw-r--r--packages/backend/src/models/ChatRoom.ts2
-rw-r--r--packages/backend/src/models/ChatRoomInvitation.ts4
-rw-r--r--packages/backend/src/models/ChatRoomMembership.ts4
-rw-r--r--packages/backend/src/models/Clip.ts2
-rw-r--r--packages/backend/src/models/ClipFavorite.ts4
-rw-r--r--packages/backend/src/models/ClipNote.ts4
-rw-r--r--packages/backend/src/models/DriveFile.ts4
-rw-r--r--packages/backend/src/models/DriveFolder.ts4
-rw-r--r--packages/backend/src/models/Flash.ts2
-rw-r--r--packages/backend/src/models/FlashLike.ts4
-rw-r--r--packages/backend/src/models/FollowRequest.ts4
-rw-r--r--packages/backend/src/models/Following.ts4
-rw-r--r--packages/backend/src/models/GalleryLike.ts4
-rw-r--r--packages/backend/src/models/GalleryPost.ts2
-rw-r--r--packages/backend/src/models/Meta.ts8
-rw-r--r--packages/backend/src/models/ModerationLog.ts2
-rw-r--r--packages/backend/src/models/Muting.ts4
-rw-r--r--packages/backend/src/models/Note.ts8
-rw-r--r--packages/backend/src/models/NoteDraft.ts8
-rw-r--r--packages/backend/src/models/NoteFavorite.ts4
-rw-r--r--packages/backend/src/models/NoteReaction.ts4
-rw-r--r--packages/backend/src/models/NoteThreadMuting.ts2
-rw-r--r--packages/backend/src/models/Page.ts4
-rw-r--r--packages/backend/src/models/PageLike.ts4
-rw-r--r--packages/backend/src/models/PasswordResetRequest.ts2
-rw-r--r--packages/backend/src/models/Poll.ts2
-rw-r--r--packages/backend/src/models/PollVote.ts4
-rw-r--r--packages/backend/src/models/PromoNote.ts2
-rw-r--r--packages/backend/src/models/PromoRead.ts4
-rw-r--r--packages/backend/src/models/RegistrationTicket.ts4
-rw-r--r--packages/backend/src/models/RegistryItem.ts2
-rw-r--r--packages/backend/src/models/RenoteMuting.ts4
-rw-r--r--packages/backend/src/models/ReversiGame.ts4
-rw-r--r--packages/backend/src/models/RoleAssignment.ts4
-rw-r--r--packages/backend/src/models/Signin.ts2
-rw-r--r--packages/backend/src/models/SwSubscription.ts2
-rw-r--r--packages/backend/src/models/SystemAccount.ts2
-rw-r--r--packages/backend/src/models/User.ts4
-rw-r--r--packages/backend/src/models/UserKeypair.ts2
-rw-r--r--packages/backend/src/models/UserList.ts2
-rw-r--r--packages/backend/src/models/UserListFavorite.ts4
-rw-r--r--packages/backend/src/models/UserListMembership.ts4
-rw-r--r--packages/backend/src/models/UserMemo.ts4
-rw-r--r--packages/backend/src/models/UserNotePining.ts4
-rw-r--r--packages/backend/src/models/UserProfile.ts4
-rw-r--r--packages/backend/src/models/UserPublickey.ts2
-rw-r--r--packages/backend/src/models/UserSecurityKey.ts2
-rw-r--r--packages/backend/src/models/Webhook.ts2
-rw-r--r--packages/backend/src/models/json-schema/meta.ts23
-rw-r--r--packages/backend/src/models/json-schema/reversi-game.ts2
-rw-r--r--packages/backend/src/models/json-schema/user.ts3
-rw-r--r--packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts4
-rw-r--r--packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts2
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts2
-rw-r--r--packages/backend/src/server/FileServerService.ts500
-rw-r--r--packages/backend/src/server/NodeinfoServerService.ts2
-rw-r--r--packages/backend/src/server/ServerModule.ts76
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts2
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts2
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts2
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts2
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts35
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/list.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-user-ips.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/ap/get.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/hashtags/users.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/change-password.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/delete-account.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications-grouped.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/update-email.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts167
-rw-r--r--packages/backend/src/server/api/openapi/schemas.ts5
-rw-r--r--packages/backend/src/server/api/stream/ChannelsService.ts94
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts93
-rw-r--r--packages/backend/src/server/api/stream/channel.ts19
-rw-r--r--packages/backend/src/server/api/stream/channels/admin.ts34
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts36
-rw-r--r--packages/backend/src/server/api/stream/channels/chat-room.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/chat-user.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/drive.ts34
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts41
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts41
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts43
-rw-r--r--packages/backend/src/server/api/stream/channels/main.ts37
-rw-r--r--packages/backend/src/server/api/stream/channels/queue-stats.ts34
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts39
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi.ts33
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts39
-rw-r--r--packages/backend/src/server/api/stream/channels/server-stats.ts34
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts49
-rw-r--r--packages/backend/src/server/file/FileServerDriveHandler.ts116
-rw-r--r--packages/backend/src/server/file/FileServerFileResolver.ts126
-rw-r--r--packages/backend/src/server/file/FileServerProxyHandler.ts272
-rw-r--r--packages/backend/src/server/file/FileServerUtils.ts107
-rw-r--r--packages/backend/src/server/oauth/OAuth2ProviderService.ts91
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts43
171 files changed, 1857 insertions, 1548 deletions
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index da585ad68d..3a33d198a5 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -86,6 +86,18 @@ if (!envOption.disableClustering) {
ev.mount();
}
+process.on('message', msg => {
+ if (msg === 'gc') {
+ if (global.gc != null) {
+ logger.info('Manual GC triggered');
+ global.gc();
+ if (process.send != null) process.send('gc ok');
+ } else {
+ logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
+ }
+ }
+});
+
readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 4776d0d412..041f58e509 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -4,8 +4,6 @@
*/
import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
@@ -17,20 +15,15 @@ import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js';
import { jobQueue, server } from './common.js';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
-
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
-function greet() {
+function greet(props: { version: string }) {
if (!envOption.quiet) {
//#region Misskey logo
- const v = `v${meta.version}`;
+ const v = `v${props.version}`;
console.log(themeColor(' _____ _ _ '));
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
@@ -46,7 +39,7 @@ function greet() {
}
bootLogger.info('Welcome to Misskey!');
- bootLogger.info(`Misskey v${meta.version}`, null, true);
+ bootLogger.info(`Misskey v${props.version}`, null, true);
}
/**
@@ -57,15 +50,15 @@ export async function masterMain() {
// initialize app
try {
- greet();
+ config = loadConfigBoot();
+ greet({ version: config.version });
showEnvironment();
await showMachineInfo(bootLogger);
showNodejsVersion();
- config = loadConfigBoot();
//await connectDb();
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
} catch (e) {
- bootLogger.error('Fatal error occurred during initialization', null, true);
+ bootLogger.error('Fatal error occurred during initialization: ' + e, null, true);
process.exit(1);
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 657d7869fa..4cd82bed87 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -219,24 +219,42 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
-const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
+/** Path of repository root directory */
+let rootDir = _dirname;
+// 見つかるまで上に遡る
+while (!fs.existsSync(resolve(rootDir, 'packages'))) {
+ const parentDir = dirname(rootDir);
+ if (parentDir === rootDir) {
+ throw new Error('Cannot find root directory');
+ }
+ rootDir = parentDir;
+}
+
+/** Path of configuration directory */
+const configDir = resolve(rootDir, '.config');
+/** Path of built directory */
+const projectBuiltDir = resolve(rootDir, 'built');
+
+const compiledConfigFilePathForTest = resolve(projectBuiltDir, '._config_.json');
-export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
+export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest)
+ ? compiledConfigFilePathForTest
+ : resolve(projectBuiltDir, '.config.json');
export function loadConfig(): Config {
if (!fs.existsSync(compiledConfigFilePath)) {
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
}
- const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
+ const meta = JSON.parse(fs.readFileSync(resolve(projectBuiltDir, 'meta.json'), 'utf-8'));
- const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
- const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
+ const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
+ const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
const frontendManifest = frontendManifestExists ?
- JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
+ JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'), 'utf-8'))
: { 'src/_boot_.ts': { file: null } };
const frontendEmbedManifest = frontendEmbedManifestExists ?
- JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
+ JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'), 'utf-8'))
: { 'src/boot.ts': { file: null } };
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
@@ -334,7 +352,7 @@ export function loadConfig(): Config {
function tryCreateUrl(url: string) {
try {
return new URL(url);
- } catch (e) {
+ } catch (_) {
throw new Error(`url="${url}" is not a valid URL.`);
}
}
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index f8e3eaf01f..5d668bc582 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -75,7 +75,7 @@ export class AccountMoveService {
*/
@bindThis
public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise<unknown> {
- const srcUri = this.userEntityService.getUserUri(src);
+ const _srcUri = this.userEntityService.getUserUri(src);
const dstUri = this.userEntityService.getUserUri(dst);
// add movedToUri to indicate that the user has moved
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index a9f6731977..f750ca212a 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -205,7 +205,7 @@ export class AnnouncementService {
announcementId: announcementId,
userId: user.id,
});
- } catch (e) {
+ } catch (_) {
return;
}
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 4efd6122b1..70a50a0175 100644
--- a/packages/backend/src/core/AvatarDecorationService.ts
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -39,7 +39,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ const { type, body: _ } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'avatarDecorationCreated':
case 'avatarDecorationUpdated':
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 87575ca59a..f075671d93 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -141,7 +141,7 @@ import { ApLoggerService } from './activitypub/ApLoggerService.js';
import { ApMfmService } from './activitypub/ApMfmService.js';
import { ApRendererService } from './activitypub/ApRendererService.js';
import { ApRequestService } from './activitypub/ApRequestService.js';
-import { ApResolverService } from './activitypub/ApResolverService.js';
+import { ApResolverService, Resolver } from './activitypub/ApResolverService.js';
import { JsonLdService } from './activitypub/JsonLdService.js';
import { RemoteLoggerService } from './RemoteLoggerService.js';
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
@@ -447,6 +447,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
+ Resolver,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
@@ -745,6 +746,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
+ Resolver,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index c7be0f7843..384704b252 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -366,7 +366,7 @@ export class EmailService {
valid: true,
reason: null,
};
- } catch (error) {
+ } catch (_) {
return {
valid: false,
reason: 'network',
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index af4d0b8c6b..c7c9f8037d 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -484,25 +484,13 @@ export class FileInfoService {
* Calculate blurhash string of image
*/
@bindThis
- private getBlurhash(path: string, type: string): Promise<string> {
- return new Promise(async (resolve, reject) => {
- (await sharpBmp(path, type))
- .raw()
- .ensureAlpha()
- .resize(64, 64, { fit: 'inside' })
- .toBuffer((err, buffer, info) => {
- if (err) return reject(err);
-
- let hash;
-
- try {
- hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
- } catch (e) {
- return reject(e);
- }
-
- resolve(hash);
- });
- });
+ private async getBlurhash(path: string, type: string): Promise<string> {
+ const sharp = await sharpBmp(path, type);
+ const { data: buffer, info } = await sharp
+ .raw()
+ .ensureAlpha()
+ .resize(64, 64, { fit: 'inside' })
+ .toBuffer({ resolveWithObject: true });
+ return blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
}
}
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index f4c747b139..da5982abf6 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -38,11 +38,7 @@ export interface BroadcastTypes {
emojis: Packed<'EmojiDetailed'>[];
};
emojiDeleted: {
- emojis: {
- id?: string;
- name: string;
- [other: string]: any;
- }[];
+ emojis: Packed<'EmojiDetailed'>[];
};
announcementCreated: {
announcement: Packed<'Announcement'>;
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index b9f1c62d9d..274966d921 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -308,7 +308,7 @@ export class MfmService {
try {
const date = new Date(parseInt(text, 10) * 1000);
return `<time datetime="${escapeHtml(date.toISOString())}">${escapeHtml(date.toISOString())}</time>`;
- } catch (err) {
+ } catch (_) {
return fnDefault(node);
}
}
@@ -376,7 +376,7 @@ export class MfmService {
try {
const url = new URL(node.props.url);
return `<a href="${escapeHtml(url.href)}">${toHtml(node.children)}</a>`;
- } catch (err) {
+ } catch (_) {
return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
}
},
@@ -390,7 +390,7 @@ export class MfmService {
try {
const url = new URL(href);
return `<a href="${escapeHtml(url.href)}" class="u-url mention">${escapeHtml(acct)}</a>`;
- } catch (err) {
+ } catch (_) {
return escapeHtml(acct);
}
},
@@ -419,7 +419,7 @@ export class MfmService {
try {
const url = new URL(node.props.url);
return `<a href="${escapeHtml(url.href)}">${escapeHtml(node.props.url)}</a>`;
- } catch (err) {
+ } catch (_) {
return escapeHtml(node.props.url);
}
},
diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts
index a346ff7618..e144138c2c 100644
--- a/packages/backend/src/core/NoteDraftService.ts
+++ b/packages/backend/src/core/NoteDraftService.ts
@@ -187,9 +187,9 @@ export class NoteDraftService {
}
//#region visibleUsers
- let visibleUsers: MiUser[] = [];
+ let _visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
- visibleUsers = await this.usersRepository.findBy({
+ _visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 42782167bb..f90ae80731 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -6,7 +6,6 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { MetricsTime, type JobType } from 'bullmq';
-import { parse as parseRedisInfo } from 'redis-info';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
@@ -86,6 +85,19 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
pattern: '0 4 * * *',
}];
+function parseRedisInfo(infoText: string): Record<string, string> {
+ const fields = infoText
+ .split('\n')
+ .filter(line => line.length > 0 && !line.startsWith('#'))
+ .map(line => line.trim().split(':'));
+
+ const result: Record<string, string> = {};
+ for (const [key, value] of fields) {
+ result[key] = value;
+ }
+ return result;
+}
+
@Injectable()
export class QueueService {
constructor(
@@ -890,7 +902,7 @@ export class QueueService {
},
db: {
version: db.redis_version,
- mode: db.redis_mode,
+ mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
runId: db.run_id,
processId: db.process_id,
port: parseInt(db.tcp_port),
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index f2f7480dfa..2ffee69c21 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -314,7 +314,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
default:
return false;
}
- } catch (err) {
+ } catch (_) {
// TODO: log error
return false;
}
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index 71dc718916..87097ada93 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -190,8 +190,7 @@ export class SearchService {
return this.searchNoteByMeiliSearch(q, me, opts, pagination);
}
default: {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const typeCheck: never = this.provider;
+ const _: never = this.provider;
return [];
}
}
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index 7920e58e36..3ecb912a64 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -49,8 +49,8 @@ export class UserSuspendService {
});
(async () => {
- await this.postSuspend(user).catch(e => {});
- await this.unFollowAll(user).catch(e => {});
+ await this.postSuspend(user).catch(_ => {});
+ await this.unFollowAll(user).catch(_ => {});
})();
}
@@ -67,7 +67,7 @@ export class UserSuspendService {
});
(async () => {
- await this.postUnsuspend(user).catch(e => {});
+ await this.postUnsuspend(user).catch(_ => {});
})();
}
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 21ea9b9983..e3ceebccae 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -98,7 +98,7 @@ export class UtilityService {
try {
// TODO: RE2インスタンスをキャッシュ
return new RE2(regexp[1], regexp[2]).test(text);
- } catch (err) {
+ } catch (_) {
// This should never happen due to input sanitisation.
return false;
}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 81637580e3..ff47ca930d 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -95,7 +95,7 @@ export class ApInboxService {
if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
if (items.length >= resolver.getRecursionLimit()) {
@@ -221,7 +221,7 @@ export class ApInboxService {
this.logger.info(`Accept: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(err => {
this.logger.error(`Resolution failed: ${err}`);
@@ -284,7 +284,7 @@ export class ApInboxService {
this.logger.info(`Announce: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
@@ -406,7 +406,7 @@ export class ApInboxService {
}
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -575,7 +575,7 @@ export class ApInboxService {
this.logger.info(`Reject: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -642,7 +642,7 @@ export class ApInboxService {
this.logger.info(`Undo: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -774,7 +774,7 @@ export class ApInboxService {
this.logger.debug('Update');
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 4570977c5d..8c461b6031 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -515,7 +515,7 @@ export class ApRendererService {
const restPart = maybeUrl.slice(match[0].length);
return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
- } catch (e) {
+ } catch (_) {
return maybeUrl;
}
};
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 49298a1d22..d14b82dc92 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -226,7 +226,7 @@ export class ApRequestService {
return await this.signedGet(href, user, allowSoftfail, false);
}
}
- } catch (e) {
+ } catch (_) {
// something went wrong parsing the HTML, ignore the whole thing
}
}
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index 646150455b..0f51b1ce8d 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -3,10 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
-import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
+import type {
+ FollowRequestsRepository,
+ MiMeta,
+ NoteReactionsRepository,
+ NotesRepository,
+ PollsRepository,
+ UsersRepository
+} from '@/models/_.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js';
@@ -16,26 +23,43 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import type { ICollection, IObject, IOrderedCollection } from './type.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
-import type { IObject, ICollection, IOrderedCollection } from './type.js';
+import { ModuleRef } from '@nestjs/core';
+@Injectable({ scope: Scope.TRANSIENT })
export class Resolver {
private history: Set<string>;
private user?: MiLocalUser;
private logger: Logger;
+ private recursionLimit = 256;
constructor(
+ @Inject(DI.config)
private config: Config,
+
+ @Inject(DI.meta)
private meta: MiMeta,
+
+ @Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+
+ @Inject(DI.notesRepository)
private notesRepository: NotesRepository,
+
+ @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
+
+ @Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
+
+ @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
+
private utilityService: UtilityService,
private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService,
@@ -43,7 +67,6 @@ export class Resolver {
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
- private recursionLimit = 256,
) {
this.history = new Set();
this.logger = this.loggerService.getLogger('ap-resolve');
@@ -180,54 +203,12 @@ export class Resolver {
@Injectable()
export class ApResolverService {
constructor(
- @Inject(DI.config)
- private config: Config,
-
- @Inject(DI.meta)
- private meta: MiMeta,
-
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
-
- @Inject(DI.pollsRepository)
- private pollsRepository: PollsRepository,
-
- @Inject(DI.noteReactionsRepository)
- private noteReactionsRepository: NoteReactionsRepository,
-
- @Inject(DI.followRequestsRepository)
- private followRequestsRepository: FollowRequestsRepository,
-
- private utilityService: UtilityService,
- private systemAccountService: SystemAccountService,
- private apRequestService: ApRequestService,
- private httpRequestService: HttpRequestService,
- private apRendererService: ApRendererService,
- private apDbResolverService: ApDbResolverService,
- private loggerService: LoggerService,
+ private moduleRef: ModuleRef,
) {
}
@bindThis
- public createResolver(): Resolver {
- return new Resolver(
- this.config,
- this.meta,
- this.usersRepository,
- this.notesRepository,
- this.pollsRepository,
- this.noteReactionsRepository,
- this.followRequestsRepository,
- this.utilityService,
- this.systemAccountService,
- this.apRequestService,
- this.httpRequestService,
- this.apRendererService,
- this.apDbResolverService,
- this.loggerService,
- );
+ public async createResolver(): Promise<Resolver> {
+ return await this.moduleRef.create(Resolver);
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index e7ece87b01..0496774c19 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -46,7 +46,7 @@ export class ApImageService {
throw new Error('actor has been suspended');
}
- const image = await this.apResolverService.createResolver().resolve(value);
+ const image = await (await this.apResolverService.createResolver()).resolve(value);
if (!isDocument(image)) return null;
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 214d32f67f..1fc5728c98 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -128,7 +128,7 @@ export class ApNoteService {
@bindThis
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(value);
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index e52078ed0f..ebe8e9c964 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -310,7 +310,7 @@ export class ApPersonService implements OnModuleInit {
}
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
@@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit {
//#endregion
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri);
@@ -678,7 +678,7 @@ export class ApPersonService implements OnModuleInit {
// リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
return await this.createPerson(uri, resolver);
}
@@ -707,7 +707,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the featured: ${user.uri}`);
- const _resolver = resolver ?? this.apResolverService.createResolver();
+ const _resolver = resolver ?? await this.apResolverService.createResolver();
// Resolve to (Ordered)Collection Object
const collection = await _resolver.resolveCollection(user.featured);
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index a2cdaf02ca..8ac2f21e26 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -45,7 +45,7 @@ export class ApQuestionService {
@bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
@@ -91,7 +91,7 @@ export class ApQuestionService {
// resolve new Question object
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(value);
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts
index cfa983e766..f69a484398 100644
--- a/packages/backend/src/core/entities/ChatEntityService.ts
+++ b/packages/backend/src/core/entities/ChatEntityService.ts
@@ -138,7 +138,7 @@ export class ChatEntityService {
const reactions: { reaction: string; }[] = [];
for (const record of message.reactions) {
- const [userId, reaction] = record.split('/');
+ const [, reaction] = record.split('/');
reactions.push({
reaction,
});
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index a6f7f369a6..1865d494c4 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -17,6 +17,7 @@ import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { IdService } from '@/core/IdService.js';
+import { uniqueByKey } from '@/misc/unique-by-key.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
import { UserEntityService } from './UserEntityService.js';
@@ -226,6 +227,7 @@ export class DriveFileEntityService {
options?: PackOptions,
hint?: {
packedUser?: Packed<'UserLite'>
+ packedFolder?: Packed<'DriveFolder'>
},
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
@@ -250,9 +252,9 @@ export class DriveFileEntityService {
thumbnailUrl: this.getThumbnailUrl(file),
comment: file.comment,
folderId: file.folderId,
- folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
+ folder: opts.detail && file.folderId ? (hint?.packedFolder ?? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
- }) : null,
+ })) : null,
userId: file.userId,
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
@@ -263,10 +265,41 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
- const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
- const _userMap = await this.userEntityService.packMany(_user)
- .then(users => new Map(users.map(user => [user.id, user])));
- const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
+ // -- ユーザ情報の事前取得 --
+
+ let userMap: Map<string, Packed<'UserLite'>> | null = null;
+ if (options?.withUser) {
+ const users = files
+ .map(({ user, userId }) => user ?? userId)
+ .filter(x => x != null);
+
+ const uniqueUsers = uniqueByKey(users, (user) => typeof user === 'string' ? user : user.id);
+ const packedUsers = await this.userEntityService.packMany(uniqueUsers);
+ userMap = new Map(packedUsers.map(user => [user.id, user]));
+ }
+
+ // -- フォルダ情報の事前取得 --
+
+ let folderMap: Map<string, Packed<'DriveFolder'>> | null = null;
+ if (options?.detail) {
+ const folders = files
+ .map(({ folder, folderId }) => folder ?? folderId)
+ .filter(x => x != null);
+
+ const uniqueFolders = uniqueByKey(folders, (folder) => typeof folder === 'string' ? folder : folder.id);
+ const packedFolders = await this.driveFolderEntityService.packMany(uniqueFolders, { detail: true });
+ folderMap = new Map(packedFolders.map(folder => [folder.id, folder]));
+ }
+
+ const items = await Promise.all(files.map(f => this.packNullable(
+ f,
+ options,
+ {
+ packedUser: f.userId ? userMap?.get(f.userId) : undefined,
+ packedFolder: f.folderId ? folderMap?.get(f.folderId) : undefined,
+ },
+ )));
+
return items.filter(x => x != null);
}
diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts
index 299f23ad38..326421e149 100644
--- a/packages/backend/src/core/entities/DriveFolderEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts
@@ -12,6 +12,9 @@ import type { } from '@/models/Blocking.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { In } from 'typeorm';
+import { uniqueByKey } from '@/misc/unique-by-key.js';
+import { splitIdAndObjects } from '@/misc/split-id-and-objects.js';
@Injectable()
export class DriveFolderEntityService {
@@ -32,12 +35,20 @@ export class DriveFolderEntityService {
options?: {
detail: boolean
},
+ hint?: {
+ folderMap?: Map<string, MiDriveFolder>;
+ foldersCountMap?: Map<string, number> | null;
+ filesCountMap?: Map<string, number> | null;
+ parentPacker?: (id: string) => Promise<Packed<'DriveFolder'>>;
+ },
): Promise<Packed<'DriveFolder'>> {
const opts = Object.assign({
detail: false,
}, options);
- const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
+ const folder = typeof src === 'object'
+ ? src
+ : hint?.folderMap?.get(src) ?? await this.driveFoldersRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: folder.id,
@@ -46,20 +57,141 @@ export class DriveFolderEntityService {
parentId: folder.parentId,
...(opts.detail ? {
- foldersCount: this.driveFoldersRepository.countBy({
- parentId: folder.id,
- }),
- filesCount: this.driveFilesRepository.countBy({
- folderId: folder.id,
- }),
+ foldersCount: hint?.foldersCountMap?.get(folder.id)
+ ?? this.driveFoldersRepository.countBy({
+ parentId: folder.id,
+ }),
+ filesCount: hint?.filesCountMap?.get(folder.id)
+ ?? this.driveFilesRepository.countBy({
+ folderId: folder.id,
+ }),
...(folder.parentId ? {
- parent: this.pack(folder.parentId, {
- detail: true,
- }),
+ parent: hint?.parentPacker
+ ? hint.parentPacker(folder.parentId)
+ : this.pack(folder.parentId, { detail: true }, hint),
} : {}),
} : {}),
});
}
-}
+ public async packMany(
+ src: Array<MiDriveFolder['id'] | MiDriveFolder>,
+ options?: {
+ detail: boolean
+ },
+ ): Promise<Array<Packed<'DriveFolder'>>> {
+ /**
+ * 重複を除去しつつ、必要なDriveFolderオブジェクトをすべて取得する
+ */
+ const collectUniqueObjects = async (src: Array<MiDriveFolder['id'] | MiDriveFolder>) => {
+ const uniqueSrc = uniqueByKey(
+ src,
+ (s) => typeof s === 'string' ? s : s.id,
+ );
+ const { ids, objects } = splitIdAndObjects(uniqueSrc);
+
+ const uniqueObjects = new Map<string, MiDriveFolder>(objects.map(s => [s.id, s]));
+ const needsFetchIds = ids.filter(id => !uniqueObjects.has(id));
+
+ if (needsFetchIds.length > 0) {
+ const fetchedObjects = await this.driveFoldersRepository.find({
+ where: {
+ id: In(needsFetchIds),
+ },
+ });
+ for (const obj of fetchedObjects) {
+ uniqueObjects.set(obj.id, obj);
+ }
+ }
+
+ return uniqueObjects;
+ };
+
+ /**
+ * 親フォルダーを再帰的に収集する
+ */
+ const collectAncestors = async (folderMap: Map<string, MiDriveFolder>) => {
+ for (;;) {
+ const parentIds = new Set<string>();
+ for (const folder of folderMap.values()) {
+ if (folder.parentId != null && !folderMap.has(folder.parentId)) {
+ parentIds.add(folder.parentId);
+ }
+ }
+
+ if (parentIds.size === 0) break;
+
+ const fetchedParents = await this.driveFoldersRepository.find({
+ where: {
+ id: In([...parentIds]),
+ },
+ });
+
+ if (fetchedParents.length === 0) break;
+
+ for (const parent of fetchedParents) {
+ folderMap.set(parent.id, parent);
+ }
+ }
+ };
+
+ const opts = Object.assign({
+ detail: false,
+ }, options);
+
+ const folderMap = await collectUniqueObjects(src);
+
+ let foldersCountMap: Map<string, number> | null = null;
+ let filesCountMap: Map<string, number> | null = null;
+ if (opts.detail) {
+ await collectAncestors(folderMap);
+
+ const ids = [...folderMap.keys()];
+ if (ids.length > 0) {
+ const folderCounts = await this.driveFoldersRepository.createQueryBuilder('folder')
+ .select('folder.parentId', 'parentId')
+ .addSelect('COUNT(*)', 'count')
+ .where('folder.parentId IN (:...ids)', { ids })
+ .groupBy('folder.parentId')
+ .getRawMany<{ parentId: string; count: string }>();
+
+ const fileCounts = await this.driveFilesRepository.createQueryBuilder('file')
+ .select('file.folderId', 'folderId')
+ .addSelect('COUNT(*)', 'count')
+ .where('file.folderId IN (:...ids)', { ids })
+ .groupBy('file.folderId')
+ .getRawMany<{ folderId: string; count: string }>();
+
+ foldersCountMap = new Map(folderCounts.map(row => [row.parentId, Number(row.count)]));
+ filesCountMap = new Map(fileCounts.map(row => [row.folderId, Number(row.count)]));
+ } else {
+ foldersCountMap = new Map();
+ filesCountMap = new Map();
+ }
+ }
+
+ const packedMap = new Map<string, Promise<Packed<'DriveFolder'>>>();
+ const packFromId = (id: string): Promise<Packed<'DriveFolder'>> => {
+ const cached = packedMap.get(id);
+ if (cached) return cached;
+
+ const folder = folderMap.get(id);
+ if (!folder) {
+ throw new Error(`DriveFolder not found: ${id}`);
+ }
+
+ const packedPromise = this.pack(folder, options, {
+ folderMap,
+ foldersCountMap,
+ filesCountMap,
+ parentPacker: packFromId,
+ });
+ packedMap.set(id, packedPromise);
+
+ return packedPromise;
+ };
+
+ return Promise.all(src.map(s => packFromId(typeof s === 'string' ? s : s.id)));
+ }
+}
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 490d3f2511..309de3b08f 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -41,7 +41,7 @@ export class EmojiEntityService {
@bindThis
public packSimpleMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
}
@@ -69,7 +69,7 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 2da614a120..8e56ddbc02 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -55,13 +55,13 @@ export class MetaEntityService {
if (instance.defaultLightTheme) {
try {
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
- } catch (e) {
+ } catch (_) {
}
}
if (instance.defaultDarkTheme) {
try {
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
- } catch (e) {
+ } catch (_) {
}
}
diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts
index 54ce4d472a..fe4926bfe3 100644
--- a/packages/backend/src/core/entities/NoteReactionEntityService.ts
+++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts
@@ -54,7 +54,7 @@ export class NoteReactionEntityService implements OnModuleInit {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReaction'>> {
- const opts = Object.assign({
+ const _opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
@@ -90,7 +90,7 @@ export class NoteReactionEntityService implements OnModuleInit {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReactionWithNote'>> {
- const opts = Object.assign({
+ const _opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index df042e75c1..21099bad3e 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
+function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
+ return ['random', '1', '2'].includes(bw);
+}
+
@Injectable()
export class ReversiGameEntityService {
constructor(
@@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
- bw: game.bw,
+ bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
@@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
- bw: game.bw,
+ bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index ac5b855096..0f4051e7b8 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -720,7 +720,7 @@ export class UserEntityService implements OnModuleInit {
me,
{
...options,
- userProfile: profilesMap.get(u.id),
+ userProfile: profilesMap?.get(u.id),
userRelations: userRelations,
userMemos: userMemos,
pinNotes: pinNotes,
diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
index c50f2b723c..0d1c7ee46e 100644
--- a/packages/backend/src/misc/check-word-mute.ts
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -56,7 +56,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
try {
return new RE2(regexp[1], regexp[2]).test(text);
- } catch (err) {
+ } catch (_) {
// This should never happen due to input sanitisation.
return false;
}
diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts
index e132fa8f31..571996973b 100644
--- a/packages/backend/src/misc/get-ip-hash.ts
+++ b/packages/backend/src/misc/get-ip-hash.ts
@@ -12,7 +12,7 @@ export function getIpHash(ip: string): string {
// (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
- } catch (e) {
+ } catch (_) {
const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, '')).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
}
diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts
index 6cbbdef74c..40067cacf5 100644
--- a/packages/backend/src/misc/i18n.ts
+++ b/packages/backend/src/misc/i18n.ts
@@ -26,7 +26,7 @@ export class I18n<T extends Record<string, any>> {
}
}
return str;
- } catch (e) {
+ } catch (_) {
console.warn(`missing localization '${key}'`);
return key;
}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index ed7d5bfc3a..cf233defd9 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -64,6 +64,7 @@ import {
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
packedMetaLiteSchema,
+ packedMetaClientOptionsSchema,
} from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
@@ -135,6 +136,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ MetaClientOptions: packedMetaClientOptionsSchema,
UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
@@ -262,8 +264,6 @@ type ObjectSchemaTypeDef<p extends Schema> =
never :
any;
-type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
-
export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'null' ? null :
p['type'] extends 'integer' ? number :
diff --git a/packages/backend/src/misc/split-id-and-objects.ts b/packages/backend/src/misc/split-id-and-objects.ts
new file mode 100644
index 0000000000..d23bb93695
--- /dev/null
+++ b/packages/backend/src/misc/split-id-and-objects.ts
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * idとオブジェクトを分離する
+ * @param input idまたはオブジェクトの配列
+ * @returns idの配列とオブジェクトの配列
+ */
+export function splitIdAndObjects<T extends { id: string }>(input: (T | string)[]): { ids: string[]; objects: T[] } {
+ const ids: string[] = [];
+ const objects : T[] = [];
+
+ for (const item of input) {
+ if (typeof item === 'string') {
+ ids.push(item);
+ } else {
+ objects.push(item);
+ }
+ }
+
+ return {
+ ids,
+ objects,
+ };
+}
diff --git a/packages/backend/src/misc/unique-by-key.ts b/packages/backend/src/misc/unique-by-key.ts
new file mode 100644
index 0000000000..4308e29d21
--- /dev/null
+++ b/packages/backend/src/misc/unique-by-key.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * itemsの中でkey関数が返す値が重複しないようにした配列を返す
+ * @param items 重複を除去したい配列
+ * @param key 重複判定に使うキーを返す関数
+ * @returns 重複を除去した配列
+ */
+export function uniqueByKey<TItem, TKey = string>(items: Iterable<TItem>, key: (item: TItem) => TKey): TItem[] {
+ const map = new Map<TKey, TItem>();
+ for (const item of items) {
+ const k = key(item);
+ if (!map.has(k)) {
+ map.set(k, item);
+ }
+ }
+ return [...map.values()];
+}
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
index 17ec6abed5..daed81c174 100644
--- a/packages/backend/src/models/AbuseReportNotificationRecipient.ts
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -67,7 +67,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザ.
*/
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
@@ -76,7 +76,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザプロフィール.
*/
- @ManyToOne(type => MiUserProfile, {
+ @ManyToOne(() => MiUserProfile, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
@@ -96,7 +96,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のシステムWebhook.
*/
- @ManyToOne(type => MiSystemWebhook, {
+ @ManyToOne(() => MiSystemWebhook, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'systemWebhookId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_systemWebhookId' })
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index d43ebf9342..cd49fcddfe 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -18,7 +18,7 @@ export class MiAbuseUserReport {
@Column(id())
public targetUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAbuseUserReport {
@Column(id())
public reporterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiAbuseUserReport {
})
public assigneeId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/AccessToken.ts b/packages/backend/src/models/AccessToken.ts
index 6f98c14ec1..a853dcc6cb 100644
--- a/packages/backend/src/models/AccessToken.ts
+++ b/packages/backend/src/models/AccessToken.ts
@@ -41,7 +41,7 @@ export class MiAccessToken {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -53,7 +53,7 @@ export class MiAccessToken {
})
public appId: MiApp['id'] | null;
- @ManyToOne(type => MiApp, {
+ @ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts
index d0c59fff50..f664c75262 100644
--- a/packages/backend/src/models/Announcement.ts
+++ b/packages/backend/src/models/Announcement.ts
@@ -79,7 +79,7 @@ export class MiAnnouncement {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/AnnouncementRead.ts b/packages/backend/src/models/AnnouncementRead.ts
index 47de8dd180..2133cff140 100644
--- a/packages/backend/src/models/AnnouncementRead.ts
+++ b/packages/backend/src/models/AnnouncementRead.ts
@@ -18,7 +18,7 @@ export class MiAnnouncementRead {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAnnouncementRead {
@Column(id())
public announcementId: MiAnnouncement['id'];
- @ManyToOne(type => MiAnnouncement, {
+ @ManyToOne(() => MiAnnouncement, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index ccc8823703..3433cf20af 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -24,7 +24,7 @@ export class MiAntenna {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -45,7 +45,7 @@ export class MiAntenna {
})
public userListId: MiUserList['id'] | null;
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/App.ts b/packages/backend/src/models/App.ts
index 0185e2995c..bbb80b99ef 100644
--- a/packages/backend/src/models/App.ts
+++ b/packages/backend/src/models/App.ts
@@ -20,7 +20,7 @@ export class MiApp {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
diff --git a/packages/backend/src/models/AuthSession.ts b/packages/backend/src/models/AuthSession.ts
index 03050ba955..a7273e63bf 100644
--- a/packages/backend/src/models/AuthSession.ts
+++ b/packages/backend/src/models/AuthSession.ts
@@ -25,7 +25,7 @@ export class MiAuthSession {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
nullable: true,
})
@@ -35,7 +35,7 @@ export class MiAuthSession {
@Column(id())
public appId: MiApp['id'];
- @ManyToOne(type => MiApp, {
+ @ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Blocking.ts b/packages/backend/src/models/Blocking.ts
index 34a6efe5a6..49b584f509 100644
--- a/packages/backend/src/models/Blocking.ts
+++ b/packages/backend/src/models/Blocking.ts
@@ -20,7 +20,7 @@ export class MiBlocking {
})
public blockeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiBlocking {
})
public blockerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts
index 686e39c118..5dd7009fc6 100644
--- a/packages/backend/src/models/BubbleGameRecord.ts
+++ b/packages/backend/src/models/BubbleGameRecord.ts
@@ -18,7 +18,7 @@ export class MiBubbleGameRecord {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts
index f5e9b17e3e..5a5b914eb1 100644
--- a/packages/backend/src/models/Channel.ts
+++ b/packages/backend/src/models/Channel.ts
@@ -27,7 +27,7 @@ export class MiChannel {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -52,7 +52,7 @@ export class MiChannel {
})
public bannerId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
+ @ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelFavorite.ts b/packages/backend/src/models/ChannelFavorite.ts
index 167f41cf16..4f49468598 100644
--- a/packages/backend/src/models/ChannelFavorite.ts
+++ b/packages/backend/src/models/ChannelFavorite.ts
@@ -20,7 +20,7 @@ export class MiChannelFavorite {
})
public channelId: MiChannel['id'];
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChannelFavorite {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelFollowing.ts b/packages/backend/src/models/ChannelFollowing.ts
index c7afdd05b0..7597e704a8 100644
--- a/packages/backend/src/models/ChannelFollowing.ts
+++ b/packages/backend/src/models/ChannelFollowing.ts
@@ -21,7 +21,7 @@ export class MiChannelFollowing {
})
public followeeId: MiChannel['id'];
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiChannelFollowing {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelMuting.ts b/packages/backend/src/models/ChannelMuting.ts
index 11ac7e5cef..b7054c9c5f 100644
--- a/packages/backend/src/models/ChannelMuting.ts
+++ b/packages/backend/src/models/ChannelMuting.ts
@@ -20,7 +20,7 @@ export class MiChannelMuting {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChannelMuting {
})
public channelId: MiChannel['id'];
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatApproval.ts b/packages/backend/src/models/ChatApproval.ts
index 55c9f07e9a..bd2509b67f 100644
--- a/packages/backend/src/models/ChatApproval.ts
+++ b/packages/backend/src/models/ChatApproval.ts
@@ -19,7 +19,7 @@ export class MiChatApproval {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -31,7 +31,7 @@ export class MiChatApproval {
})
public otherId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts
index 3d2b64268e..530ef9b842 100644
--- a/packages/backend/src/models/ChatMessage.ts
+++ b/packages/backend/src/models/ChatMessage.ts
@@ -20,7 +20,7 @@ export class MiChatMessage {
})
public fromUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatMessage {
})
public toUserId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -44,7 +44,7 @@ export class MiChatMessage {
})
public toRoomId: MiChatRoom['id'] | null;
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -72,7 +72,7 @@ export class MiChatMessage {
})
public fileId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
+ @ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts
index ad2a910b78..c148b16af8 100644
--- a/packages/backend/src/models/ChatRoom.ts
+++ b/packages/backend/src/models/ChatRoom.ts
@@ -23,7 +23,7 @@ export class MiChatRoom {
})
public ownerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoomInvitation.ts b/packages/backend/src/models/ChatRoomInvitation.ts
index 36ce12bc92..5827d0401d 100644
--- a/packages/backend/src/models/ChatRoomInvitation.ts
+++ b/packages/backend/src/models/ChatRoomInvitation.ts
@@ -20,7 +20,7 @@ export class MiChatRoomInvitation {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomInvitation {
})
public roomId: MiChatRoom['id'];
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts
index 3cb5524859..d59b4426df 100644
--- a/packages/backend/src/models/ChatRoomMembership.ts
+++ b/packages/backend/src/models/ChatRoomMembership.ts
@@ -20,7 +20,7 @@ export class MiChatRoomMembership {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomMembership {
})
public roomId: MiChatRoom['id'];
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Clip.ts b/packages/backend/src/models/Clip.ts
index 6295a329fb..ddd0298f44 100644
--- a/packages/backend/src/models/Clip.ts
+++ b/packages/backend/src/models/Clip.ts
@@ -25,7 +25,7 @@ export class MiClip {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ClipFavorite.ts b/packages/backend/src/models/ClipFavorite.ts
index 40bdb9f4aa..2d46fd0f0e 100644
--- a/packages/backend/src/models/ClipFavorite.ts
+++ b/packages/backend/src/models/ClipFavorite.ts
@@ -18,7 +18,7 @@ export class MiClipFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiClipFavorite {
@Column(id())
public clipId: MiClip['id'];
- @ManyToOne(type => MiClip, {
+ @ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ClipNote.ts b/packages/backend/src/models/ClipNote.ts
index 6e1d2bec4c..23df66c4e0 100644
--- a/packages/backend/src/models/ClipNote.ts
+++ b/packages/backend/src/models/ClipNote.ts
@@ -21,7 +21,7 @@ export class MiClipNote {
})
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiClipNote {
})
public clipId: MiClip['id'];
- @ManyToOne(type => MiClip, {
+ @ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index 7b03e3e494..79189b10eb 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -22,7 +22,7 @@ export class MiDriveFile {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -142,7 +142,7 @@ export class MiDriveFile {
})
public folderId: MiDriveFolder['id'] | null;
- @ManyToOne(type => MiDriveFolder, {
+ @ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/DriveFolder.ts b/packages/backend/src/models/DriveFolder.ts
index 07046d6e11..7e34c07f46 100644
--- a/packages/backend/src/models/DriveFolder.ts
+++ b/packages/backend/src/models/DriveFolder.ts
@@ -26,7 +26,7 @@ export class MiDriveFolder {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiDriveFolder {
})
public parentId: MiDriveFolder['id'] | null;
- @ManyToOne(type => MiDriveFolder, {
+ @ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index 5db7dca992..ed677a9de3 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -38,7 +38,7 @@ export class MiFlash {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/FlashLike.ts b/packages/backend/src/models/FlashLike.ts
index a9fb48123e..0d99c2a9ae 100644
--- a/packages/backend/src/models/FlashLike.ts
+++ b/packages/backend/src/models/FlashLike.ts
@@ -18,7 +18,7 @@ export class MiFlashLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiFlashLike {
@Column(id())
public flashId: MiFlash['id'];
- @ManyToOne(type => MiFlash, {
+ @ManyToOne(() => MiFlash, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/FollowRequest.ts b/packages/backend/src/models/FollowRequest.ts
index 3ff5e7a478..468829b7e8 100644
--- a/packages/backend/src/models/FollowRequest.ts
+++ b/packages/backend/src/models/FollowRequest.ts
@@ -20,7 +20,7 @@ export class MiFollowRequest {
})
public followeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiFollowRequest {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts
index 62cbc29f26..fe62166287 100644
--- a/packages/backend/src/models/Following.ts
+++ b/packages/backend/src/models/Following.ts
@@ -21,7 +21,7 @@ export class MiFollowing {
})
public followeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiFollowing {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/GalleryLike.ts b/packages/backend/src/models/GalleryLike.ts
index ed0963122d..787b38e46d 100644
--- a/packages/backend/src/models/GalleryLike.ts
+++ b/packages/backend/src/models/GalleryLike.ts
@@ -18,7 +18,7 @@ export class MiGalleryLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiGalleryLike {
@Column(id())
public postId: MiGalleryPost['id'];
- @ManyToOne(type => MiGalleryPost, {
+ @ManyToOne(() => MiGalleryPost, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/GalleryPost.ts b/packages/backend/src/models/GalleryPost.ts
index 04d8823e37..f66956628b 100644
--- a/packages/backend/src/models/GalleryPost.ts
+++ b/packages/backend/src/models/GalleryPost.ts
@@ -36,7 +36,7 @@ export class MiGalleryPost {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 205c9eeb89..620853450c 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -21,7 +21,7 @@ export class MiMeta {
})
public rootUserId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
@@ -725,7 +725,11 @@ export class MiMeta {
@Column('jsonb', {
default: { },
})
- public clientOptions: Record<string, any>;
+ public clientOptions: {
+ entrancePageStyle: 'classic' | 'simple';
+ showTimelineForVisitor: boolean;
+ showActivitiesForVisitor: boolean;
+ };
}
export type SoftwareSuspension = {
diff --git a/packages/backend/src/models/ModerationLog.ts b/packages/backend/src/models/ModerationLog.ts
index edde315fdf..c22114a36d 100644
--- a/packages/backend/src/models/ModerationLog.ts
+++ b/packages/backend/src/models/ModerationLog.ts
@@ -16,7 +16,7 @@ export class MiModerationLog {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Muting.ts b/packages/backend/src/models/Muting.ts
index e1240b9c4e..9406b97a62 100644
--- a/packages/backend/src/models/Muting.ts
+++ b/packages/backend/src/models/Muting.ts
@@ -26,7 +26,7 @@ export class MiMuting {
})
public muteeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -39,7 +39,7 @@ export class MiMuting {
})
public muterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index 23e5960b60..089fe8f188 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -35,7 +35,7 @@ export class MiNote {
})
public replyId: MiNote['id'] | null;
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -49,7 +49,7 @@ export class MiNote {
})
public renoteId: MiNote['id'] | null;
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -83,7 +83,7 @@ export class MiNote {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -208,7 +208,7 @@ export class MiNote {
})
public channelId: MiChannel['id'] | null;
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts
index f078e8c21b..5bfd9699fe 100644
--- a/packages/backend/src/models/NoteDraft.ts
+++ b/packages/backend/src/models/NoteDraft.ts
@@ -27,7 +27,7 @@ export class MiNoteDraft {
public replyId: MiNote['id'] | null;
// There is a possibility that replyId is not null but reply is null when the reply note is deleted.
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -42,7 +42,7 @@ export class MiNoteDraft {
public renoteId: MiNote['id'] | null;
// There is a possibility that renoteId is not null but renote is null when the renote note is deleted.
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -66,7 +66,7 @@ export class MiNoteDraft {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -120,7 +120,7 @@ export class MiNoteDraft {
// There is a possibility that channelId is not null but channel is null when the channel is deleted.
// (deleting channel is not implemented so it's not happening now but may happen in the future)
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
createForeignKeyConstraints: false,
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteFavorite.ts b/packages/backend/src/models/NoteFavorite.ts
index cf76c767b0..0e498eb70d 100644
--- a/packages/backend/src/models/NoteFavorite.ts
+++ b/packages/backend/src/models/NoteFavorite.ts
@@ -18,7 +18,7 @@ export class MiNoteFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiNoteFavorite {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts
index 42dfcaa9ad..98263081ab 100644
--- a/packages/backend/src/models/NoteReaction.ts
+++ b/packages/backend/src/models/NoteReaction.ts
@@ -18,7 +18,7 @@ export class MiNoteReaction {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiNoteReaction {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteThreadMuting.ts b/packages/backend/src/models/NoteThreadMuting.ts
index e7bd39f348..32bb829c0b 100644
--- a/packages/backend/src/models/NoteThreadMuting.ts
+++ b/packages/backend/src/models/NoteThreadMuting.ts
@@ -19,7 +19,7 @@ export class MiNoteThreadMuting {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts
index d46f6e9d16..8811200801 100644
--- a/packages/backend/src/models/Page.ts
+++ b/packages/backend/src/models/Page.ts
@@ -56,7 +56,7 @@ export class MiPage {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -68,7 +68,7 @@ export class MiPage {
})
public eyeCatchingImageId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
+ @ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PageLike.ts b/packages/backend/src/models/PageLike.ts
index 05ca22cf2c..cf3025ae1c 100644
--- a/packages/backend/src/models/PageLike.ts
+++ b/packages/backend/src/models/PageLike.ts
@@ -18,7 +18,7 @@ export class MiPageLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiPageLike {
@Column(id())
public pageId: MiPage['id'];
- @ManyToOne(type => MiPage, {
+ @ManyToOne(() => MiPage, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PasswordResetRequest.ts b/packages/backend/src/models/PasswordResetRequest.ts
index fdaf21056b..3379b540ee 100644
--- a/packages/backend/src/models/PasswordResetRequest.ts
+++ b/packages/backend/src/models/PasswordResetRequest.ts
@@ -24,7 +24,7 @@ export class MiPasswordResetRequest {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts
index ca985c8b24..d82e29fb85 100644
--- a/packages/backend/src/models/Poll.ts
+++ b/packages/backend/src/models/Poll.ts
@@ -15,7 +15,7 @@ export class MiPoll {
@PrimaryColumn(id())
public noteId: MiNote['id'];
- @OneToOne(type => MiNote, {
+ @OneToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PollVote.ts b/packages/backend/src/models/PollVote.ts
index b5c780293c..600ca8ea41 100644
--- a/packages/backend/src/models/PollVote.ts
+++ b/packages/backend/src/models/PollVote.ts
@@ -18,7 +18,7 @@ export class MiPollVote {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiPollVote {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PromoNote.ts b/packages/backend/src/models/PromoNote.ts
index ae27adec9e..871f7471fc 100644
--- a/packages/backend/src/models/PromoNote.ts
+++ b/packages/backend/src/models/PromoNote.ts
@@ -13,7 +13,7 @@ export class MiPromoNote {
@PrimaryColumn(id())
public noteId: MiNote['id'];
- @OneToOne(type => MiNote, {
+ @OneToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PromoRead.ts b/packages/backend/src/models/PromoRead.ts
index b2a698cc7b..15a3573ef3 100644
--- a/packages/backend/src/models/PromoRead.ts
+++ b/packages/backend/src/models/PromoRead.ts
@@ -18,7 +18,7 @@ export class MiPromoRead {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiPromoRead {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RegistrationTicket.ts b/packages/backend/src/models/RegistrationTicket.ts
index 0a4e4b9189..07216599d3 100644
--- a/packages/backend/src/models/RegistrationTicket.ts
+++ b/packages/backend/src/models/RegistrationTicket.ts
@@ -23,7 +23,7 @@ export class MiRegistrationTicket {
})
public expiresAt: Date | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -36,7 +36,7 @@ export class MiRegistrationTicket {
})
public createdById: MiUser['id'] | null;
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RegistryItem.ts b/packages/backend/src/models/RegistryItem.ts
index 335e8b9eab..869980bbff 100644
--- a/packages/backend/src/models/RegistryItem.ts
+++ b/packages/backend/src/models/RegistryItem.ts
@@ -25,7 +25,7 @@ export class MiRegistryItem {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RenoteMuting.ts b/packages/backend/src/models/RenoteMuting.ts
index 448a0b7663..b760a09c53 100644
--- a/packages/backend/src/models/RenoteMuting.ts
+++ b/packages/backend/src/models/RenoteMuting.ts
@@ -20,7 +20,7 @@ export class MiRenoteMuting {
})
public muteeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiRenoteMuting {
})
public muterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index 6b29a0ce8c..fbbf24792f 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -27,7 +27,7 @@ export class MiReversiGame {
@Column(id())
public user1Id: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -36,7 +36,7 @@ export class MiReversiGame {
@Column(id())
public user2Id: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RoleAssignment.ts b/packages/backend/src/models/RoleAssignment.ts
index 37755d631b..cb96377f66 100644
--- a/packages/backend/src/models/RoleAssignment.ts
+++ b/packages/backend/src/models/RoleAssignment.ts
@@ -21,7 +21,7 @@ export class MiRoleAssignment {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiRoleAssignment {
})
public roleId: MiRole['id'];
- @ManyToOne(type => MiRole, {
+ @ManyToOne(() => MiRole, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Signin.ts b/packages/backend/src/models/Signin.ts
index f8ff9c57d7..59cbad735d 100644
--- a/packages/backend/src/models/Signin.ts
+++ b/packages/backend/src/models/Signin.ts
@@ -16,7 +16,7 @@ export class MiSignin {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/SwSubscription.ts b/packages/backend/src/models/SwSubscription.ts
index 0c531132b3..a95aede44f 100644
--- a/packages/backend/src/models/SwSubscription.ts
+++ b/packages/backend/src/models/SwSubscription.ts
@@ -16,7 +16,7 @@ export class MiSwSubscription {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/SystemAccount.ts b/packages/backend/src/models/SystemAccount.ts
index f32880b81d..2a48e62ed1 100644
--- a/packages/backend/src/models/SystemAccount.ts
+++ b/packages/backend/src/models/SystemAccount.ts
@@ -18,7 +18,7 @@ export class MiSystemAccount {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index a6e9edcf5f..084dd35485 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -99,7 +99,7 @@ export class MiUser {
})
public avatarId: MiDriveFile['id'] | null;
- @OneToOne(type => MiDriveFile, {
+ @OneToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -112,7 +112,7 @@ export class MiUser {
})
public bannerId: MiDriveFile['id'] | null;
- @OneToOne(type => MiDriveFile, {
+ @OneToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts
index f5252d126c..894739c84c 100644
--- a/packages/backend/src/models/UserKeypair.ts
+++ b/packages/backend/src/models/UserKeypair.ts
@@ -12,7 +12,7 @@ export class MiUserKeypair {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserList.ts b/packages/backend/src/models/UserList.ts
index 5fb991a87d..05fd833b6f 100644
--- a/packages/backend/src/models/UserList.ts
+++ b/packages/backend/src/models/UserList.ts
@@ -25,7 +25,7 @@ export class MiUserList {
})
public isPublic: boolean;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserListFavorite.ts b/packages/backend/src/models/UserListFavorite.ts
index 80b2d61eb7..67ab92d98c 100644
--- a/packages/backend/src/models/UserListFavorite.ts
+++ b/packages/backend/src/models/UserListFavorite.ts
@@ -18,7 +18,7 @@ export class MiUserListFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiUserListFavorite {
@Column(id())
public userListId: MiUserList['id'];
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserListMembership.ts b/packages/backend/src/models/UserListMembership.ts
index af659d071d..1a2b3fffc1 100644
--- a/packages/backend/src/models/UserListMembership.ts
+++ b/packages/backend/src/models/UserListMembership.ts
@@ -21,7 +21,7 @@ export class MiUserListMembership {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiUserListMembership {
})
public userListId: MiUserList['id'];
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserMemo.ts b/packages/backend/src/models/UserMemo.ts
index 29e28d290a..facc8c6b1c 100644
--- a/packages/backend/src/models/UserMemo.ts
+++ b/packages/backend/src/models/UserMemo.ts
@@ -20,7 +20,7 @@ export class MiUserMemo {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiUserMemo {
})
public targetUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserNotePining.ts b/packages/backend/src/models/UserNotePining.ts
index 92c5cd55d0..950da2ad22 100644
--- a/packages/backend/src/models/UserNotePining.ts
+++ b/packages/backend/src/models/UserNotePining.ts
@@ -18,7 +18,7 @@ export class MiUserNotePining {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiUserNotePining {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 501b539210..b05bf14ef9 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -17,7 +17,7 @@ export class MiUserProfile {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -215,7 +215,7 @@ export class MiUserProfile {
})
public pinnedPageId: MiPage['id'] | null;
- @OneToOne(type => MiPage, {
+ @OneToOne(() => MiPage, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts
index 6bcd785304..8c23d368e9 100644
--- a/packages/backend/src/models/UserPublickey.ts
+++ b/packages/backend/src/models/UserPublickey.ts
@@ -12,7 +12,7 @@ export class MiUserPublickey {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserSecurityKey.ts b/packages/backend/src/models/UserSecurityKey.ts
index 0babbe1abe..577ec359e4 100644
--- a/packages/backend/src/models/UserSecurityKey.ts
+++ b/packages/backend/src/models/UserSecurityKey.ts
@@ -18,7 +18,7 @@ export class MiUserSecurityKey {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index b4cab4edc8..5f833115cc 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -22,7 +22,7 @@ export class MiWebhook {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index a0e7d490b3..0c3ec141bc 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -72,8 +72,7 @@ export const packedMetaLiteSchema = {
optional: false, nullable: true,
},
clientOptions: {
- type: 'object',
- optional: false, nullable: false,
+ ref: 'MetaClientOptions',
},
disableRegistration: {
type: 'boolean',
@@ -397,3 +396,23 @@ export const packedMetaDetailedSchema = {
},
],
} as const;
+
+export const packedMetaClientOptionsSchema = {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ entrancePageStyle: {
+ type: 'string',
+ enum: ['classic', 'simple'],
+ optional: false, nullable: false,
+ },
+ showTimelineForVisitor: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ showActivitiesForVisitor: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts
index cb37200384..378ae41cb5 100644
--- a/packages/backend/src/models/json-schema/reversi-game.ts
+++ b/packages/backend/src/models/json-schema/reversi-game.ts
@@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
+ enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
@@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
+ enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index b5fd38a7d7..f71ec1d023 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -618,6 +618,9 @@ export const packedMeDetailedOnlySchema = {
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
+ login: { optional: true, ...notificationRecieveConfig },
+ createToken: { optional: true, ...notificationRecieveConfig },
+ exportCompleted: { optional: true, ...notificationRecieveConfig },
},
},
emailNotificationTypes: {
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index e237cd4975..53ecd2d180 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -123,8 +123,8 @@ export class ExportCustomEmojisProcessorService {
metaStream.end();
// Create archive
- await new Promise<void>(async (resolve) => {
- const [archivePath, archiveCleanup] = await createTemp();
+ const [archivePath, archiveCleanup] = await createTemp();
+ await new Promise<void>((resolve) => {
const archiveStream = fs.createWriteStream(archivePath);
const archive = archiver('zip', {
zlib: { level: 0 },
diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts
index d0eaeee090..719a09980c 100644
--- a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts
+++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts
@@ -63,7 +63,7 @@ export class PostScheduledNoteProcessorService {
this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', {
noteId: note.id,
});
- } catch (err) {
+ } catch (_) {
this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', {
noteDraftId: draft.id,
});
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index a5fb5b82e3..54ffeecc6b 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -116,7 +116,7 @@ export class ActivityPubServerService {
try {
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
- } catch (e) {
+ } catch (_) {
reply.code(401);
return;
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 772c37094c..f5034d0733 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -7,27 +7,22 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
-import rename from 'rename';
-import sharp from 'sharp';
-import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
-import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
-import { createTemp } from '@/misc/create-temp.js';
-import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js';
-import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
-import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js';
-import { contentDisposition } from '@/misc/content-disposition.js';
import { FileInfoService } from '@/core/FileInfoService.js';
+import { ImageProcessingService } from '@/core/ImageProcessingService.js';
+import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
-import { isMimeImage } from '@/misc/is-mime-image.js';
-import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
+import { FileServerDriveHandler } from './file/FileServerDriveHandler.js';
+import { FileServerFileResolver } from './file/FileServerFileResolver.js';
+import { FileServerProxyHandler } from './file/FileServerProxyHandler.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
@@ -38,6 +33,9 @@ const assets = `${_dirname}/../../server/file/assets/`;
@Injectable()
export class FileServerService {
private logger: Logger;
+ private driveHandler: FileServerDriveHandler;
+ private proxyHandler: FileServerProxyHandler;
+ private fileResolver: FileServerFileResolver;
constructor(
@Inject(DI.config)
@@ -54,6 +52,24 @@ export class FileServerService {
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
+ this.fileResolver = new FileServerFileResolver(
+ this.driveFilesRepository,
+ this.fileInfoService,
+ this.downloadService,
+ this.internalStorageService,
+ );
+ this.driveHandler = new FileServerDriveHandler(
+ this.config,
+ this.fileResolver,
+ assets,
+ this.videoProcessingService,
+ );
+ this.proxyHandler = new FileServerProxyHandler(
+ this.config,
+ this.fileResolver,
+ assets,
+ this.imageProcessingService,
+ );
//this.createServer = this.createServer.bind(this);
}
@@ -78,7 +94,7 @@ export class FileServerService {
});
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
- return await this.sendDriveFile(request, reply)
+ return await this.driveHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
@@ -91,7 +107,7 @@ export class FileServerService {
Params: { url: string; };
Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => {
- return await this.proxyHandler(request, reply)
+ return await this.proxyHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
@@ -116,462 +132,4 @@ export class FileServerService {
reply.code(500);
return;
}
-
- @bindThis
- private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
- const key = request.params.key;
- const file = await this.getFileFromKey(key).then();
-
- if (file === '404') {
- reply.code(404);
- reply.header('Cache-Control', 'max-age=86400');
- return reply.sendFile('/dummy.png', assets);
- }
-
- if (file === '204') {
- reply.code(204);
- reply.header('Cache-Control', 'max-age=86400');
- return;
- }
-
- try {
- if (file.state === 'remote') {
- let image: IImageStreamable | null = null;
-
- if (file.fileRole === 'thumbnail') {
- if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
- reply.header('Cache-Control', 'max-age=31536000, immutable');
-
- const url = new URL(`${this.config.mediaProxy}/static.webp`);
- url.searchParams.set('url', file.url);
- url.searchParams.set('static', '1');
-
- file.cleanup();
- return await reply.redirect(url.toString(), 301);
- } else if (file.mime.startsWith('video/')) {
- const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
- if (externalThumbnail) {
- file.cleanup();
- return await reply.redirect(externalThumbnail, 301);
- }
-
- image = await this.videoProcessingService.generateVideoThumbnail(file.path);
- }
- }
-
- if (file.fileRole === 'webpublic') {
- if (['image/svg+xml'].includes(file.mime)) {
- reply.header('Cache-Control', 'max-age=31536000, immutable');
-
- const url = new URL(`${this.config.mediaProxy}/svg.webp`);
- url.searchParams.set('url', file.url);
-
- file.cleanup();
- return await reply.redirect(url.toString(), 301);
- }
- }
-
- if (!image) {
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
-
- image = {
- data: fs.createReadStream(file.path, {
- start,
- end,
- }),
- ext: file.ext,
- type: file.mime,
- };
-
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- } else {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- }
- }
-
- if ('pipe' in image.data && typeof image.data.pipe === 'function') {
- // image.dataがstreamなら、stream終了後にcleanup
- image.data.on('end', file.cleanup);
- image.data.on('close', file.cleanup);
- } else {
- // image.dataがstreamでないなら直ちにcleanup
- file.cleanup();
- }
-
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
- reply.header('Content-Length', file.file.size);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition',
- contentDisposition(
- 'inline',
- correctFilename(file.filename, image.ext),
- ),
- );
- return image.data;
- }
-
- if (file.fileRole !== 'original') {
- const filename = rename(file.filename, {
- suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
- extname: file.ext ? `.${file.ext}` : '.unknown',
- }).toString();
-
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', filename));
-
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
- const fileStream = fs.createReadStream(file.path, {
- start,
- end,
- });
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- return fileStream;
- }
-
- return fs.createReadStream(file.path);
- } else {
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
- reply.header('Content-Length', file.file.size);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', file.filename));
-
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
- const fileStream = fs.createReadStream(file.path, {
- start,
- end,
- });
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- return fileStream;
- }
-
- return fs.createReadStream(file.path);
- }
- } catch (e) {
- if ('cleanup' in file) file.cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
- const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
-
- if (typeof url !== 'string') {
- reply.code(400);
- return;
- }
-
- // アバタークロップなど、どうしてもオリジンである必要がある場合
- const mustOrigin = 'origin' in request.query;
-
- if (this.config.externalMediaProxyEnabled && !mustOrigin) {
- // 外部のメディアプロキシが有効なら、そちらにリダイレクト
-
- reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
-
- const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
-
- for (const [key, value] of Object.entries(request.query)) {
- url.searchParams.append(key, value);
- }
-
- return await reply.redirect(
- url.toString(),
- 301,
- );
- }
-
- if (!request.headers['user-agent']) {
- throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
- } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
- throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
- }
-
- // Create temp file
- const file = await this.getStreamAndTypeFromUrl(url);
- if (file === '404') {
- reply.code(404);
- reply.header('Cache-Control', 'max-age=86400');
- return reply.sendFile('/dummy.png', assets);
- }
-
- if (file === '204') {
- reply.code(204);
- reply.header('Cache-Control', 'max-age=86400');
- return;
- }
-
- try {
- const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
- const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
-
- if (
- 'emoji' in request.query ||
- 'avatar' in request.query ||
- 'static' in request.query ||
- 'preview' in request.query ||
- 'badge' in request.query
- ) {
- if (!isConvertibleImage) {
- // 画像でないなら404でお茶を濁す
- throw new StatusError('Unexpected mime', 404);
- }
- }
-
- let image: IImageStreamable | null = null;
- if ('emoji' in request.query || 'avatar' in request.query) {
- if (!isAnimationConvertibleImage && !('static' in request.query)) {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- } else {
- const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
- .resize({
- height: 'emoji' in request.query ? 128 : 320,
- withoutEnlargement: true,
- })
- .webp(webpDefault);
-
- image = {
- data,
- ext: 'webp',
- type: 'image/webp',
- };
- }
- } else if ('static' in request.query) {
- image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
- } else if ('preview' in request.query) {
- image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
- } else if ('badge' in request.query) {
- const mask = (await sharpBmp(file.path, file.mime))
- .resize(96, 96, {
- fit: 'contain',
- position: 'centre',
- withoutEnlargement: false,
- })
- .greyscale()
- .normalise()
- .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
- .flatten({ background: '#000' })
- .toColorspace('b-w');
-
- const stats = await mask.clone().stats();
-
- if (stats.entropy < 0.1) {
- // エントロピーがあまりない場合は404にする
- throw new StatusError('Skip to provide badge', 404);
- }
-
- const data = sharp({
- create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
- })
- .pipelineColorspace('b-w')
- .boolean(await mask.png().toBuffer(), 'eor');
-
- image = {
- data: await data.png().toBuffer(),
- ext: 'png',
- type: 'image/png',
- };
- } else if (file.mime === 'image/svg+xml') {
- image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
- } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
- throw new StatusError('Rejected type', 403, 'Rejected type');
- }
-
- if (!image) {
- if (request.headers.range && file.file && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
-
- image = {
- data: fs.createReadStream(file.path, {
- start,
- end,
- }),
- ext: file.ext,
- type: file.mime,
- };
-
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- } else {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- }
- }
-
- if ('cleanup' in file) {
- if ('pipe' in image.data && typeof image.data.pipe === 'function') {
- // image.dataがstreamなら、stream終了後にcleanup
- image.data.on('end', file.cleanup);
- image.data.on('close', file.cleanup);
- } else {
- // image.dataがstreamでないなら直ちにcleanup
- file.cleanup();
- }
- }
-
- reply.header('Content-Type', image.type);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition',
- contentDisposition(
- 'inline',
- correctFilename(file.filename, image.ext),
- ),
- );
- return image.data;
- } catch (e) {
- if ('cleanup' in file) file.cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async getStreamAndTypeFromUrl(url: string): Promise<
- { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
- | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
- | '404'
- | '204'
- > {
- if (url.startsWith(`${this.config.url}/files/`)) {
- const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
- if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
-
- return await this.getFileFromKey(key);
- }
-
- return await this.downloadAndDetectTypeFromUrl(url);
- }
-
- @bindThis
- private async downloadAndDetectTypeFromUrl(url: string): Promise<
- { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
- > {
- const [path, cleanup] = await createTemp();
- try {
- const { filename } = await this.downloadService.downloadUrl(url, path);
-
- const { mime, ext } = await this.fileInfoService.detectType(path);
-
- return {
- state: 'remote',
- mime, ext,
- path, cleanup,
- filename,
- };
- } catch (e) {
- cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async getFileFromKey(key: string): Promise<
- { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
- | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
- | '404'
- | '204'
- > {
- // Fetch drive file
- const file = await this.driveFilesRepository.createQueryBuilder('file')
- .where('file.accessKey = :accessKey', { accessKey: key })
- .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
- .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
- .getOne();
-
- if (file == null) return '404';
-
- const isThumbnail = file.thumbnailAccessKey === key;
- const isWebpublic = file.webpublicAccessKey === key;
-
- if (!file.storedInternal) {
- if (!(file.isLink && file.uri)) return '204';
- const result = await this.downloadAndDetectTypeFromUrl(file.uri);
- file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
- return {
- ...result,
- url: file.uri,
- fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
- file,
- filename: file.name,
- };
- }
-
- const path = this.internalStorageService.resolvePath(key);
-
- if (isThumbnail || isWebpublic) {
- const { mime, ext } = await this.fileInfoService.detectType(path);
- return {
- state: 'stored_internal',
- fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
- file,
- filename: file.name,
- mime, ext,
- path,
- };
- }
-
- return {
- state: 'stored_internal',
- fileRole: 'original',
- file,
- filename: file.name,
- // 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
- mime: this.fileInfoService.fixMime(file.type),
- ext: null,
- path,
- };
- }
}
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index 239ef82dec..93c36f5365 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -48,8 +48,6 @@ export class NodeinfoServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const nodeinfo2 = async (version: number) => {
- const now = Date.now();
-
const notesChart = await this.notesChart.getChart('hour', 1, null);
const localPosts = notesChart.local.total[0];
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 111421472d..8259a2a9e4 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -13,7 +13,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { GetterService } from './api/GetterService.js';
-import { ChannelsService } from './api/stream/ChannelsService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { ApiLoggerService } from './api/ApiLoggerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@@ -31,24 +30,25 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
-import { MainChannelService } from './api/stream/channels/main.js';
-import { AdminChannelService } from './api/stream/channels/admin.js';
-import { AntennaChannelService } from './api/stream/channels/antenna.js';
-import { ChannelChannelService } from './api/stream/channels/channel.js';
-import { DriveChannelService } from './api/stream/channels/drive.js';
-import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
-import { HashtagChannelService } from './api/stream/channels/hashtag.js';
-import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
-import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
-import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
-import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
-import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
-import { UserListChannelService } from './api/stream/channels/user-list.js';
-import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
-import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
-import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
-import { ReversiChannelService } from './api/stream/channels/reversi.js';
-import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
+import MainStreamConnection from '@/server/api/stream/Connection.js';
+import { MainChannel } from './api/stream/channels/main.js';
+import { AdminChannel } from './api/stream/channels/admin.js';
+import { AntennaChannel } from './api/stream/channels/antenna.js';
+import { ChannelChannel } from './api/stream/channels/channel.js';
+import { DriveChannel } from './api/stream/channels/drive.js';
+import { GlobalTimelineChannel } from './api/stream/channels/global-timeline.js';
+import { HashtagChannel } from './api/stream/channels/hashtag.js';
+import { HomeTimelineChannel } from './api/stream/channels/home-timeline.js';
+import { HybridTimelineChannel } from './api/stream/channels/hybrid-timeline.js';
+import { LocalTimelineChannel } from './api/stream/channels/local-timeline.js';
+import { QueueStatsChannel } from './api/stream/channels/queue-stats.js';
+import { ServerStatsChannel } from './api/stream/channels/server-stats.js';
+import { UserListChannel } from './api/stream/channels/user-list.js';
+import { RoleTimelineChannel } from './api/stream/channels/role-timeline.js';
+import { ChatUserChannel } from './api/stream/channels/chat-user.js';
+import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
+import { ReversiChannel } from './api/stream/channels/reversi.js';
+import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
@@ -69,7 +69,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
ServerService,
WellKnownServerService,
GetterService,
- ChannelsService,
+ MainStreamConnection,
ApiCallService,
ApiLoggerService,
ApiServerService,
@@ -80,24 +80,24 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
SigninService,
SignupApiService,
StreamingApiServerService,
- MainChannelService,
- AdminChannelService,
- AntennaChannelService,
- ChannelChannelService,
- DriveChannelService,
- GlobalTimelineChannelService,
- HashtagChannelService,
- RoleTimelineChannelService,
- ChatUserChannelService,
- ChatRoomChannelService,
- ReversiChannelService,
- ReversiGameChannelService,
- HomeTimelineChannelService,
- HybridTimelineChannelService,
- LocalTimelineChannelService,
- QueueStatsChannelService,
- ServerStatsChannelService,
- UserListChannelService,
+ MainChannel,
+ AdminChannel,
+ AntennaChannel,
+ ChannelChannel,
+ DriveChannel,
+ GlobalTimelineChannel,
+ HashtagChannel,
+ RoleTimelineChannel,
+ ChatUserChannel,
+ ChatRoomChannel,
+ ReversiChannel,
+ ReversiGameChannel,
+ HomeTimelineChannel,
+ HybridTimelineChannel,
+ LocalTimelineChannel,
+ QueueStatsChannel,
+ ServerStatsChannel,
+ UserListChannel,
OpenApiServerService,
OAuth2ProviderService,
],
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 8bae46d9fb..0ccb3df631 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -426,7 +426,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try {
data[k] = JSON.parse(data[k]);
- } catch (e) {
+ } catch (_) {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 00e8828242..5c9d16a95a 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -231,7 +231,7 @@ export class SigninApiService {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index 920f9d0b3a..6feb4c3afa 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -93,7 +93,7 @@ export class SigninWithPasskeyApiService {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
- } catch (err) {
+ } catch (_) {
reply.code(429);
return {
error: {
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 53336a087d..b419c51ef1 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -255,7 +255,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'EXPIRED');
}
- const { account, secret } = await this.signupService.signup({
+ const { account } = await this.signupService.signup({
username: pendingUser.username,
passwordHash: pendingUser.password,
});
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 21f2f0b7e2..8a317bdc4e 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -8,18 +8,14 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, MiAccessToken } from '@/models/_.js';
-import { NotificationService } from '@/core/NotificationService.js';
+import type { MiAccessToken } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
-import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
-import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
-import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
-import MainStreamConnection from './stream/Connection.js';
-import { ChannelsService } from './stream/ChannelsService.js';
+import MainStreamConnection, { ConnectionRequest } from './stream/Connection.js';
import type * as http from 'node:http';
+import { ContextIdFactory, ModuleRef } from '@nestjs/core';
@Injectable()
export class StreamingApiServerService {
@@ -31,16 +27,9 @@ export class StreamingApiServerService {
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private cacheService: CacheService,
+ private moduleRef: ModuleRef,
private authenticateService: AuthenticateService,
- private channelsService: ChannelsService,
- private notificationService: NotificationService,
private usersService: UserService,
- private channelFollowingService: ChannelFollowingService,
- private channelMutingService: ChannelMutingService,
) {
}
@@ -94,14 +83,12 @@ export class StreamingApiServerService {
return;
}
- const stream = new MainStreamConnection(
- this.channelsService,
- this.notificationService,
- this.cacheService,
- this.channelFollowingService,
- this.channelMutingService,
- user, app,
- );
+ const contextId = ContextIdFactory.create();
+ this.moduleRef.registerRequestByContextId<ConnectionRequest>({
+ user,
+ token: app,
+ }, contextId);
+ const stream = await this.moduleRef.create(MainStreamConnection, contextId);
await stream.init();
@@ -124,7 +111,7 @@ export class StreamingApiServerService {
user: MiLocalUser | null;
app: MiAccessToken | null
}) => {
- const { stream, user, app } = ctx;
+ const { stream, user } = ctx;
const ev = new EventEmitter();
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index 9aecc0f0fd..6679005c3c 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -391,6 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
+export * as 'users/get-following-users-by-birthday' from './endpoints/users/get-following-users-by-birthday.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index b8bfda73a4..74462b302a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- const { raw, packed } = await this.announcementService.create({
+ const { packed } = await this.announcementService.create({
updatedAt: null,
title: ps.title,
text: ps.text,
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index 804bd5d9b9..aeebceed5a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -51,11 +51,13 @@ export const meta = {
},
icon: {
type: 'string',
- optional: false, nullable: true,
+ optional: false, nullable: false,
+ enum: ['info', 'warning', 'error', 'success'],
},
display: {
type: 'string',
optional: false, nullable: false,
+ enum: ['normal', 'banner', 'dialog'],
},
isActive: {
type: 'boolean',
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index cf03859ce5..d4305e7d7c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
// Create file
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
- } catch (e) {
+ } catch (_) {
// TODO: need to return Drive Error
throw new ApiError();
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 660aa55bf8..b9448b4bc2 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 34d200455e..658367409c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 7bde10af46..e20bc21f6b 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
- const mustBeNever: never = error;
+ const _mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
index b7781b8c99..bdd0ee6cac 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
kind: 'read:admin:user-ips',
res: {
type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 2c7f793584..5beed3a7e8 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -428,8 +428,7 @@ export const meta = {
optional: false, nullable: true,
},
clientOptions: {
- type: 'object',
- optional: false, nullable: false,
+ ref: 'MetaClientOptions',
},
description: {
type: 'string',
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index b3c2cecc67..372fe3a25f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Injectable, Inject } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -67,7 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
- clientOptions: { type: 'object', nullable: false },
+ clientOptions: {
+ type: 'object', nullable: false,
+ properties: {
+ entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
+ showTimelineForVisitor: { type: 'boolean', nullable: false },
+ showActivitiesForVisitor: { type: 'boolean', nullable: false },
+ },
+ },
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@@ -217,6 +225,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private metaService: MetaService,
private moderationLogService: ModerationLogService,
) {
@@ -329,7 +340,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.clientOptions !== undefined) {
- set.clientOptions = ps.clientOptions;
+ set.clientOptions = {
+ ...this.serverSettings.clientOptions,
+ ...ps.clientOptions,
+ };
}
if (ps.cacheRemoteFiles !== undefined) {
diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts
index 14286bc23e..ff03fce72b 100644
--- a/packages/backend/src/server/api/endpoints/ap/get.ts
+++ b/packages/backend/src/server/api/endpoints/ap/get.ts
@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apResolverService: ApResolverService,
) {
super(meta, paramDef, async (ps, me) => {
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(ps.uri);
return object;
});
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index fe48e7497a..47da6b4fbd 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
if (err instanceof IdentifiableError) {
@@ -215,7 +215,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
type: 'Note',
object,
};
- } catch (e) {
+ } catch (_) {
return null;
}
}
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index 30f0c1b0c8..7b2c137bd4 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
tag: { type: 'string' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
@@ -74,7 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
}
- const users = await query.limit(ps.limit).getMany();
+ const users = await query
+ .limit(ps.limit)
+ .offset(ps.offset)
+ .getMany();
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 65eece5b97..8dc5cafb56 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 9391aee5e0..050dbaf49e 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -212,7 +212,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a54c598213..b6c837eda7 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index c350136eae..6e5d9943de 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index b5a53cc889..23b577dc18 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -57,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index bb78d47149..19ea187ee8 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index bfa0b4605d..42324c7778 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index f933eaab00..4fe39bb8e8 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -71,7 +71,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
- const EXTRA_LIMIT = 100;
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined);
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index da1faee30d..c2f4281f36 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 9971a1ea4d..5207d9f2b0 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -323,7 +323,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
new RE2(regexp[1], regexp[2]);
- } catch (err) {
+ } catch (_) {
throw new ApiError(meta.errors.invalidRegexp);
}
}
@@ -587,7 +587,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
})
.execute();
}
- } catch (err) {
+ } catch (_) {
// なにもしない
}
}
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
index 29c6aa7434..7c0dddb827 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
- const mutedNotes = await this.notesRepository.find({
+ const _mutedNotes = await this.notesRepository.find({
where: [{
id: note.threadId ?? note.id,
}, {
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 047f9a053b..4defcc9dcf 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -86,7 +86,7 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- birthday: { ...birthdaySchema, nullable: true },
+ birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-users-by-birthday instead.' },
},
},
],
@@ -146,15 +146,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee');
+ // @deprecated use get-following-users-by-birthday instead.
if (ps.birthday) {
- try {
- const birthday = ps.birthday.substring(5, 10);
- const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
- birthdayUserQuery.select('user_profile.userId')
- .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
+ query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
- query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
- } catch (err) {
+ try {
+ const birthday = ps.birthday.split('-');
+ birthday.shift(); // 年の部分を削除
+ // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
+ } catch (_) {
throw new ApiError(meta.errors.birthdayInvalid);
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts
new file mode 100644
index 0000000000..947c19d81e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts
@@ -0,0 +1,167 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type {
+ FollowingsRepository,
+ UserProfilesRepository,
+} from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type { Packed } from '@/misc/json-schema.js';
+
+export const meta = {
+ tags: ['users'],
+
+ requireCredential: true,
+ kind: 'read:account',
+
+ description: 'Retrieve users who have a birthday on the specified range.',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ birthday: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', default: 0 },
+ birthday: {
+ oneOf: [{
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ }, {
+ type: 'object',
+ properties: {
+ begin: {
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ },
+ end: {
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ },
+ },
+ required: ['begin', 'end'],
+ }],
+ },
+ },
+ required: ['birthday'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
+
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.followingsRepository
+ .createQueryBuilder('following')
+ .andWhere('following.followerId = :userId', { userId: me.id })
+ .innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
+
+ if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
+ const range = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
+
+ // 誕生日は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)でインデックスが効くようになっているので、その形式に変換
+ const begin = range.begin.month * 100 + range.begin.day;
+ const end = range.end.month * 100 + range.end.day;
+
+ if (begin <= end) {
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin, end });
+ } else {
+ // 12/31 から 1/1 の範囲を取得するために OR で対応
+ query.andWhere(new Brackets(qb => {
+ qb.where('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND 1231', { begin });
+ qb.orWhere('get_birthday_date(followeeProfile.birthday) BETWEEN 101 AND :end', { end });
+ }));
+ }
+ } else {
+ const { month, day } = ps.birthday as { month: number; day: number };
+ // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
+ }
+
+ query.select('following.followeeId', 'user_id');
+ query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
+ query.orderBy('birthday_date', 'ASC');
+
+ const birthdayUsers = await query
+ .offset(ps.offset).limit(ps.limit)
+ .getRawMany<{ birthday_date: number; user_id: string }>();
+
+ const users = new Map<string, Packed<'UserLite'>>((
+ await this.userEntityService.packMany(
+ birthdayUsers.map(u => u.user_id),
+ me,
+ { schema: 'UserLite' },
+ )
+ ).map(u => [u.id, u]));
+
+ return birthdayUsers
+ .map(item => {
+ const birthday = new Date();
+ birthday.setHours(0, 0, 0, 0);
+ // item.birthday_date は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)で出力されるので、日付に戻してDateオブジェクトに設定
+ birthday.setMonth(Math.floor(item.birthday_date / 100) - 1, item.birthday_date % 100);
+
+ if (birthday.getTime() < new Date().setHours(0, 0, 0, 0)) {
+ birthday.setFullYear(new Date().getFullYear() + 1);
+ }
+
+ const birthdayStr = `${birthday.getFullYear()}-${(birthday.getMonth() + 1).toString().padStart(2, '0')}-${(birthday.getDate()).toString().padStart(2, '0')}`;
+ return {
+ id: item.user_id,
+ birthday: birthdayStr,
+ user: users.get(item.user_id),
+ };
+ })
+ .filter(item => item.user != null)
+ .map(item => item as { id: string; birthday: string; user: Packed<'UserLite'> });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index 1cdcbebd1a..0714f61294 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -9,9 +9,8 @@ import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { optional, nullable, ref, selfRef, ..._res }: any = schema;
- const res = deepClone(_res);
+ const { optional, nullable, ref, selfRef, ...res1 }: any = schema;
+ const res = deepClone(res1);
if (schema.type === 'object' && schema.properties) {
if (type === 'res') {
diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts
index c0ef589dea..63ad9281b2 100644
--- a/packages/backend/src/server/api/stream/ChannelsService.ts
+++ b/packages/backend/src/server/api/stream/ChannelsService.ts
@@ -4,72 +4,54 @@
*/
import { Injectable } from '@nestjs/common';
+import { HybridTimelineChannel } from './channels/hybrid-timeline.js';
+import { LocalTimelineChannel } from './channels/local-timeline.js';
+import { HomeTimelineChannel } from './channels/home-timeline.js';
+import { GlobalTimelineChannel } from './channels/global-timeline.js';
+import { MainChannel } from './channels/main.js';
+import { ChannelChannel } from './channels/channel.js';
+import { AdminChannel } from './channels/admin.js';
+import { ServerStatsChannel } from './channels/server-stats.js';
+import { QueueStatsChannel } from './channels/queue-stats.js';
+import { UserListChannel } from './channels/user-list.js';
+import { AntennaChannel } from './channels/antenna.js';
+import { DriveChannel } from './channels/drive.js';
+import { HashtagChannel } from './channels/hashtag.js';
+import { RoleTimelineChannel } from './channels/role-timeline.js';
+import { ChatUserChannel } from './channels/chat-user.js';
+import { ChatRoomChannel } from './channels/chat-room.js';
+import { ReversiChannel } from './channels/reversi.js';
+import { ReversiGameChannel } from './channels/reversi-game.js';
+import type { ChannelConstructor } from './channel.js';
import { bindThis } from '@/decorators.js';
-import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
-import { LocalTimelineChannelService } from './channels/local-timeline.js';
-import { HomeTimelineChannelService } from './channels/home-timeline.js';
-import { GlobalTimelineChannelService } from './channels/global-timeline.js';
-import { MainChannelService } from './channels/main.js';
-import { ChannelChannelService } from './channels/channel.js';
-import { AdminChannelService } from './channels/admin.js';
-import { ServerStatsChannelService } from './channels/server-stats.js';
-import { QueueStatsChannelService } from './channels/queue-stats.js';
-import { UserListChannelService } from './channels/user-list.js';
-import { AntennaChannelService } from './channels/antenna.js';
-import { DriveChannelService } from './channels/drive.js';
-import { HashtagChannelService } from './channels/hashtag.js';
-import { RoleTimelineChannelService } from './channels/role-timeline.js';
-import { ChatUserChannelService } from './channels/chat-user.js';
-import { ChatRoomChannelService } from './channels/chat-room.js';
-import { ReversiChannelService } from './channels/reversi.js';
-import { ReversiGameChannelService } from './channels/reversi-game.js';
-import { type MiChannelService } from './channel.js';
@Injectable()
export class ChannelsService {
constructor(
- private mainChannelService: MainChannelService,
- private homeTimelineChannelService: HomeTimelineChannelService,
- private localTimelineChannelService: LocalTimelineChannelService,
- private hybridTimelineChannelService: HybridTimelineChannelService,
- private globalTimelineChannelService: GlobalTimelineChannelService,
- private userListChannelService: UserListChannelService,
- private hashtagChannelService: HashtagChannelService,
- private roleTimelineChannelService: RoleTimelineChannelService,
- private antennaChannelService: AntennaChannelService,
- private channelChannelService: ChannelChannelService,
- private driveChannelService: DriveChannelService,
- private serverStatsChannelService: ServerStatsChannelService,
- private queueStatsChannelService: QueueStatsChannelService,
- private adminChannelService: AdminChannelService,
- private chatUserChannelService: ChatUserChannelService,
- private chatRoomChannelService: ChatRoomChannelService,
- private reversiChannelService: ReversiChannelService,
- private reversiGameChannelService: ReversiGameChannelService,
) {
}
@bindThis
- public getChannelService(name: string): MiChannelService<boolean> {
+ public getChannelConstructor(name: string): ChannelConstructor<boolean> {
switch (name) {
- case 'main': return this.mainChannelService;
- case 'homeTimeline': return this.homeTimelineChannelService;
- case 'localTimeline': return this.localTimelineChannelService;
- case 'hybridTimeline': return this.hybridTimelineChannelService;
- case 'globalTimeline': return this.globalTimelineChannelService;
- case 'userList': return this.userListChannelService;
- case 'hashtag': return this.hashtagChannelService;
- case 'roleTimeline': return this.roleTimelineChannelService;
- case 'antenna': return this.antennaChannelService;
- case 'channel': return this.channelChannelService;
- case 'drive': return this.driveChannelService;
- case 'serverStats': return this.serverStatsChannelService;
- case 'queueStats': return this.queueStatsChannelService;
- case 'admin': return this.adminChannelService;
- case 'chatUser': return this.chatUserChannelService;
- case 'chatRoom': return this.chatRoomChannelService;
- case 'reversi': return this.reversiChannelService;
- case 'reversiGame': return this.reversiGameChannelService;
+ case 'main': return MainChannel;
+ case 'homeTimeline': return HomeTimelineChannel;
+ case 'localTimeline': return LocalTimelineChannel;
+ case 'hybridTimeline': return HybridTimelineChannel;
+ case 'globalTimeline': return GlobalTimelineChannel;
+ case 'userList': return UserListChannel;
+ case 'hashtag': return HashtagChannel;
+ case 'roleTimeline': return RoleTimelineChannel;
+ case 'antenna': return AntennaChannel;
+ case 'channel': return ChannelChannel;
+ case 'drive': return DriveChannel;
+ case 'serverStats': return ServerStatsChannel;
+ case 'queueStats': return QueueStatsChannel;
+ case 'admin': return AdminChannel;
+ case 'chatUser': return ChatUserChannel;
+ case 'chatRoom': return ChatRoomChannel;
+ case 'reversi': return ReversiChannel;
+ case 'reversiGame': return ReversiGameChannel;
default:
throw new Error(`no such channel: ${name}`);
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 222086c960..5989409997 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -6,19 +6,39 @@
import * as WebSocket from 'ws';
import type { MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
-import type { Packed } from '@/misc/json-schema.js';
-import type { NotificationService } from '@/core/NotificationService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
-import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
-import type { ChannelsService } from './ChannelsService.js';
+import { isJsonObject } from '@/misc/json-value.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
+import type { ChannelConstructor } from './channel.js';
+import type { ChannelRequest } from './channel.js';
+import { ContextIdFactory, ModuleRef, REQUEST } from '@nestjs/core';
+import { Inject, Injectable, Scope } from '@nestjs/common';
+import { MainChannel } from '@/server/api/stream/channels/main.js';
+import { HomeTimelineChannel } from '@/server/api/stream/channels/home-timeline.js';
+import { LocalTimelineChannel } from '@/server/api/stream/channels/local-timeline.js';
+import { HybridTimelineChannel } from '@/server/api/stream/channels/hybrid-timeline.js';
+import { GlobalTimelineChannel } from '@/server/api/stream/channels/global-timeline.js';
+import { UserListChannel } from '@/server/api/stream/channels/user-list.js';
+import { HashtagChannel } from '@/server/api/stream/channels/hashtag.js';
+import { RoleTimelineChannel } from '@/server/api/stream/channels/role-timeline.js';
+import { AntennaChannel } from '@/server/api/stream/channels/antenna.js';
+import { ChannelChannel } from '@/server/api/stream/channels/channel.js';
+import { DriveChannel } from '@/server/api/stream/channels/drive.js';
+import { ServerStatsChannel } from '@/server/api/stream/channels/server-stats.js';
+import { QueueStatsChannel } from '@/server/api/stream/channels/queue-stats.js';
+import { AdminChannel } from '@/server/api/stream/channels/admin.js';
+import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
+import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
+import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
+import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
const MAX_CHANNELS_PER_CONNECTION = 32;
@@ -26,6 +46,7 @@ const MAX_CHANNELS_PER_CONNECTION = 32;
* Main stream connection
*/
// eslint-disable-next-line import/no-default-export
+@Injectable({ scope: Scope.TRANSIENT })
export default class Connection {
public user?: MiUser;
public token?: MiAccessToken;
@@ -44,16 +65,16 @@ export default class Connection {
private fetchIntervalId: NodeJS.Timeout | null = null;
constructor(
- private channelsService: ChannelsService,
+ private moduleRef: ModuleRef,
private notificationService: NotificationService,
private cacheService: CacheService,
private channelFollowingService: ChannelFollowingService,
private channelMutingService: ChannelMutingService,
- user: MiUser | null | undefined,
- token: MiAccessToken | null | undefined,
+ @Inject(REQUEST)
+ request: ConnectionRequest,
) {
- if (user) this.user = user;
- if (token) this.token = token;
+ if (request.user) this.user = request.user;
+ if (request.token) this.token = request.token;
}
@bindThis
@@ -118,7 +139,7 @@ export default class Connection {
try {
obj = JSON.parse(data.toString());
- } catch (e) {
+ } catch (_) {
return;
}
@@ -232,28 +253,34 @@ export default class Connection {
* チャンネルに接続
*/
@bindThis
- public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
+ public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
return;
}
- const channelService = this.channelsService.getChannelService(channel);
+ const channelConstructor = this.getChannelConstructor(channel);
- if (channelService.requireCredential && this.user == null) {
+ if (channelConstructor.requireCredential && this.user == null) {
return;
}
- if (this.token && ((channelService.kind && !this.token.permission.some(p => p === channelService.kind))
- || (!channelService.kind && channelService.requireCredential))) {
+ if (this.token && ((channelConstructor.kind && !this.token.permission.some(p => p === channelConstructor.kind))
+ || (!channelConstructor.kind && channelConstructor.requireCredential))) {
return;
}
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
- if (channelService.shouldShare && this.channels.some(c => c.chName === channel)) {
+ if (channelConstructor.shouldShare && this.channels.some(c => c.chName === channel)) {
return;
}
- const ch: Channel = channelService.create(id, this);
+ const contextId = ContextIdFactory.create();
+ this.moduleRef.registerRequestByContextId<ChannelRequest>({
+ id: id,
+ connection: this,
+ }, contextId);
+ const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId);
+
this.channels.push(ch);
ch.init(params ?? {});
@@ -264,6 +291,33 @@ export default class Connection {
}
}
+ @bindThis
+ public getChannelConstructor(name: string): ChannelConstructor<boolean> {
+ switch (name) {
+ case 'main': return MainChannel;
+ case 'homeTimeline': return HomeTimelineChannel;
+ case 'localTimeline': return LocalTimelineChannel;
+ case 'hybridTimeline': return HybridTimelineChannel;
+ case 'globalTimeline': return GlobalTimelineChannel;
+ case 'userList': return UserListChannel;
+ case 'hashtag': return HashtagChannel;
+ case 'roleTimeline': return RoleTimelineChannel;
+ case 'antenna': return AntennaChannel;
+ case 'channel': return ChannelChannel;
+ case 'drive': return DriveChannel;
+ case 'serverStats': return ServerStatsChannel;
+ case 'queueStats': return QueueStatsChannel;
+ case 'admin': return AdminChannel;
+ case 'chatUser': return ChatUserChannel;
+ case 'chatRoom': return ChatRoomChannel;
+ case 'reversi': return ReversiChannel;
+ case 'reversiGame': return ReversiGameChannel;
+
+ default:
+ throw new Error(`no such channel: ${name}`);
+ }
+ }
+
/**
* チャンネルから切断
* @param id チャンネルコネクションID
@@ -306,3 +360,8 @@ export default class Connection {
}
}
}
+
+export interface ConnectionRequest {
+ user: MiUser | null | undefined,
+ token: MiAccessToken | null | undefined,
+}
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 465ed4238c..86b073414d 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -22,7 +22,7 @@ export default abstract class Channel {
public abstract readonly chName: string;
public static readonly shouldShare: boolean;
public static readonly requireCredential: boolean;
- public static readonly kind?: string | null;
+ public static readonly kind: string | null;
protected get user() {
return this.connection.user;
@@ -85,9 +85,9 @@ export default abstract class Channel {
return false;
}
- constructor(id: string, connection: Connection) {
- this.id = id;
- this.connection = connection;
+ constructor(request: ChannelRequest) {
+ this.id = request.id;
+ this.connection = request.connection;
}
public send(payload: { type: string, body: JsonValue }): void;
@@ -111,9 +111,14 @@ export default abstract class Channel {
public onMessage?(type: string, body: JsonValue): void;
}
-export type MiChannelService<T extends boolean> = {
+export interface ChannelRequest {
+ id: string,
+ connection: Connection,
+}
+
+export interface ChannelConstructor<T extends boolean> {
+ new(...args: any[]): Channel;
shouldShare: boolean;
requireCredential: T;
kind: T extends true ? string : string | null | undefined;
- create: (id: string, connection: Connection) => Channel;
-};
+}
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 355d5dba21..821888cca0 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -3,17 +3,26 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class AdminChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class AdminChannel extends Channel {
public readonly chName = 'admin';
public static shouldShare = true;
public static requireCredential = true as const;
public static kind = 'read:admin:stream';
+ constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+ ) {
+ super(request);
+ }
+
@bindThis
public async init(params: JsonObject) {
// Subscribe admin stream
@@ -22,22 +31,3 @@ class AdminChannel extends Channel {
});
}
}
-
-@Injectable()
-export class AdminChannelService implements MiChannelService<true> {
- public readonly shouldShare = AdminChannel.shouldShare;
- public readonly requireCredential = AdminChannel.requireCredential;
- public readonly kind = AdminChannel.kind;
-
- constructor(
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): AdminChannel {
- return new AdminChannel(
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index e08562fdf9..ece9d2c8b1 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -3,14 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class AntennaChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class AntennaChannel extends Channel {
public readonly chName = 'antenna';
public static shouldShare = false;
public static requireCredential = true as const;
@@ -18,12 +20,12 @@ class AntennaChannel extends Channel {
private antennaId: string;
constructor(
- private noteEntityService: NoteEntityService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(request);
//this.onEvent = this.onEvent.bind(this);
}
@@ -55,24 +57,3 @@ class AntennaChannel extends Channel {
this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
}
}
-
-@Injectable()
-export class AntennaChannelService implements MiChannelService<true> {
- public readonly shouldShare = AntennaChannel.shouldShare;
- public readonly requireCredential = AntennaChannel.requireCredential;
- public readonly kind = AntennaChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): AntennaChannel {
- return new AntennaChannel(
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index c07eaac98d..1706b17526 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
@@ -11,20 +11,23 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class ChannelChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ChannelChannel extends Channel {
public readonly chName = 'channel';
public static shouldShare = false;
public static requireCredential = false as const;
private channelId: string;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private noteEntityService: NoteEntityService,
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -92,24 +95,3 @@ class ChannelChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class ChannelChannelService implements MiChannelService<false> {
- public readonly shouldShare = ChannelChannel.shouldShare;
- public readonly requireCredential = ChannelChannel.requireCredential;
- public readonly kind = ChannelChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ChannelChannel {
- return new ChannelChannel(
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/chat-room.ts b/packages/backend/src/server/api/stream/channels/chat-room.ts
index eda333dd30..7f949032e2 100644
--- a/packages/backend/src/server/api/stream/channels/chat-room.ts
+++ b/packages/backend/src/server/api/stream/channels/chat-room.ts
@@ -3,14 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { ChatService } from '@/core/ChatService.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class ChatRoomChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ChatRoomChannel extends Channel {
public readonly chName = 'chatRoom';
public static shouldShare = false;
public static requireCredential = true as const;
@@ -18,12 +20,12 @@ class ChatRoomChannel extends Channel {
private roomId: string;
constructor(
- private chatService: ChatService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private chatService: ChatService,
) {
- super(id, connection);
+ super(request);
}
@bindThis
@@ -55,24 +57,3 @@ class ChatRoomChannel extends Channel {
this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent);
}
}
-
-@Injectable()
-export class ChatRoomChannelService implements MiChannelService<true> {
- public readonly shouldShare = ChatRoomChannel.shouldShare;
- public readonly requireCredential = ChatRoomChannel.requireCredential;
- public readonly kind = ChatRoomChannel.kind;
-
- constructor(
- private chatService: ChatService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ChatRoomChannel {
- return new ChatRoomChannel(
- this.chatService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/chat-user.ts b/packages/backend/src/server/api/stream/channels/chat-user.ts
index 5323484ed7..36f3f67b28 100644
--- a/packages/backend/src/server/api/stream/channels/chat-user.ts
+++ b/packages/backend/src/server/api/stream/channels/chat-user.ts
@@ -3,14 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { ChatService } from '@/core/ChatService.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class ChatUserChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ChatUserChannel extends Channel {
public readonly chName = 'chatUser';
public static shouldShare = false;
public static requireCredential = true as const;
@@ -18,12 +20,12 @@ class ChatUserChannel extends Channel {
private otherId: string;
constructor(
- private chatService: ChatService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private chatService: ChatService,
) {
- super(id, connection);
+ super(request);
}
@bindThis
@@ -55,24 +57,3 @@ class ChatUserChannel extends Channel {
this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
}
}
-
-@Injectable()
-export class ChatUserChannelService implements MiChannelService<true> {
- public readonly shouldShare = ChatUserChannel.shouldShare;
- public readonly requireCredential = ChatUserChannel.requireCredential;
- public readonly kind = ChatUserChannel.kind;
-
- constructor(
- private chatService: ChatService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ChatUserChannel {
- return new ChatUserChannel(
- this.chatService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 03768f3d23..6f2eb2c8f9 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -3,17 +3,26 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class DriveChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class DriveChannel extends Channel {
public readonly chName = 'drive';
public static shouldShare = true;
public static requireCredential = true as const;
public static kind = 'read:account';
+ constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+ ) {
+ super(request);
+ }
+
@bindThis
public async init(params: JsonObject) {
// Subscribe drive stream
@@ -22,22 +31,3 @@ class DriveChannel extends Channel {
});
}
}
-
-@Injectable()
-export class DriveChannelService implements MiChannelService<true> {
- public readonly shouldShare = DriveChannel.shouldShare;
- public readonly requireCredential = DriveChannel.requireCredential;
- public readonly kind = DriveChannel.kind;
-
- constructor(
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): DriveChannel {
- return new DriveChannel(
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index d7c781ad12..be6be1b1e7 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -11,9 +11,11 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class GlobalTimelineChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class GlobalTimelineChannel extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = false;
public static requireCredential = false as const;
@@ -21,14 +23,14 @@ class GlobalTimelineChannel extends Channel {
private withFiles: boolean;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
-
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -74,28 +76,3 @@ class GlobalTimelineChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class GlobalTimelineChannelService implements MiChannelService<false> {
- public readonly shouldShare = GlobalTimelineChannel.shouldShare;
- public readonly requireCredential = GlobalTimelineChannel.requireCredential;
- public readonly kind = GlobalTimelineChannel.kind;
-
- constructor(
- private metaService: MetaService,
- private roleService: RoleService,
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): GlobalTimelineChannel {
- return new GlobalTimelineChannel(
- this.metaService,
- this.roleService,
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index c911d63642..1456b4f262 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -3,28 +3,30 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class HashtagChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class HashtagChannel extends Channel {
public readonly chName = 'hashtag';
public static shouldShare = false;
public static requireCredential = false as const;
private q: string[][];
constructor(
- private noteEntityService: NoteEntityService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -62,24 +64,3 @@ class HashtagChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class HashtagChannelService implements MiChannelService<false> {
- public readonly shouldShare = HashtagChannel.shouldShare;
- public readonly requireCredential = HashtagChannel.requireCredential;
- public readonly kind = HashtagChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): HashtagChannel {
- return new HashtagChannel(
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index eb5b4a8c6c..665c11b692 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -3,15 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class HomeTimelineChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class HomeTimelineChannel extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = false;
public static requireCredential = true as const;
@@ -20,12 +22,12 @@ class HomeTimelineChannel extends Channel {
private withFiles: boolean;
constructor(
- private noteEntityService: NoteEntityService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -98,24 +100,3 @@ class HomeTimelineChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class HomeTimelineChannelService implements MiChannelService<true> {
- public readonly shouldShare = HomeTimelineChannel.shouldShare;
- public readonly requireCredential = HomeTimelineChannel.requireCredential;
- public readonly kind = HomeTimelineChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): HomeTimelineChannel {
- return new HomeTimelineChannel(
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 2155e02012..54250d2a90 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -11,9 +11,11 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class HybridTimelineChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = false;
public static requireCredential = true as const;
@@ -23,14 +25,14 @@ class HybridTimelineChannel extends Channel {
private withFiles: boolean;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
-
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -118,28 +120,3 @@ class HybridTimelineChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class HybridTimelineChannelService implements MiChannelService<true> {
- public readonly shouldShare = HybridTimelineChannel.shouldShare;
- public readonly requireCredential = HybridTimelineChannel.requireCredential;
- public readonly kind = HybridTimelineChannel.kind;
-
- constructor(
- private metaService: MetaService,
- private roleService: RoleService,
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): HybridTimelineChannel {
- return new HybridTimelineChannel(
- this.metaService,
- this.roleService,
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 3d7ed6acdb..b394e9663f 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -11,25 +11,27 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class LocalTimelineChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
- public static shouldShare = false;
+ public static shouldShare = false as const;
public static requireCredential = false as const;
private withRenotes: boolean;
private withReplies: boolean;
private withFiles: boolean;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
-
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -84,28 +86,3 @@ class LocalTimelineChannel extends Channel {
this.subscriber.off('notesStream', this.onNote);
}
}
-
-@Injectable()
-export class LocalTimelineChannelService implements MiChannelService<false> {
- public readonly shouldShare = LocalTimelineChannel.shouldShare;
- public readonly requireCredential = LocalTimelineChannel.requireCredential;
- public readonly kind = LocalTimelineChannel.kind;
-
- constructor(
- private metaService: MetaService,
- private roleService: RoleService,
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): LocalTimelineChannel {
- return new LocalTimelineChannel(
- this.metaService,
- this.roleService,
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index 525f24c105..2ce53ac288 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -3,26 +3,28 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class MainChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class MainChannel extends Channel {
public readonly chName = 'main';
public static shouldShare = true;
public static requireCredential = true as const;
public static kind = 'read:account';
constructor(
- private noteEntityService: NoteEntityService,
+ @Inject(REQUEST)
+ request: ChannelRequest,
- id: string,
- connection: Channel['connection'],
+ private noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(request);
}
@bindThis
@@ -61,24 +63,3 @@ class MainChannel extends Channel {
});
}
}
-
-@Injectable()
-export class MainChannelService implements MiChannelService<true> {
- public readonly shouldShare = MainChannel.shouldShare;
- public readonly requireCredential = MainChannel.requireCredential;
- public readonly kind = MainChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): MainChannel {
- return new MainChannel(
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 91b62255b4..a87863f26c 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -4,21 +4,26 @@
*/
import Xev from 'xev';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
const ev = new Xev();
-class QueueStatsChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class QueueStatsChannel extends Channel {
public readonly chName = 'queueStats';
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+ ) {
+ super(request);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -56,22 +61,3 @@ class QueueStatsChannel extends Channel {
ev.removeListener('queueStats', this.onStats);
}
}
-
-@Injectable()
-export class QueueStatsChannelService implements MiChannelService<false> {
- public readonly shouldShare = QueueStatsChannel.shouldShare;
- public readonly requireCredential = QueueStatsChannel.requireCredential;
- public readonly kind = QueueStatsChannel.kind;
-
- constructor(
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): QueueStatsChannel {
- return new QueueStatsChannel(
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index 7597a1cfa3..58fc16e98c 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -3,31 +3,32 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { MiReversiGame } from '@/models/_.js';
-import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
import { reversiUpdateKeys } from 'misskey-js';
+import { REQUEST } from '@nestjs/core';
-class ReversiGameChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ReversiGameChannel extends Channel {
public readonly chName = 'reversiGame';
public static shouldShare = false;
public static requireCredential = false as const;
private gameId: MiReversiGame['id'] | null = null;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
-
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
}
@bindThis
@@ -107,25 +108,3 @@ class ReversiGameChannel extends Channel {
}
}
-@Injectable()
-export class ReversiGameChannelService implements MiChannelService<false> {
- public readonly shouldShare = ReversiGameChannel.shouldShare;
- public readonly requireCredential = ReversiGameChannel.requireCredential;
- public readonly kind = ReversiGameChannel.kind;
-
- constructor(
- private reversiService: ReversiService,
- private reversiGameEntityService: ReversiGameEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ReversiGameChannel {
- return new ReversiGameChannel(
- this.reversiService,
- this.reversiGameEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 6e88939724..5eff73eeef 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -3,22 +3,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class ReversiChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ReversiChannel extends Channel {
public readonly chName = 'reversi';
public static shouldShare = true;
public static requireCredential = true as const;
public static kind = 'read:account';
constructor(
- id: string,
- connection: Channel['connection'],
+ @Inject(REQUEST)
+ request: ChannelRequest,
) {
- super(id, connection);
+ super(request);
}
@bindThis
@@ -32,22 +34,3 @@ class ReversiChannel extends Channel {
this.subscriber.off(`reversiStream:${this.user!.id}`, this.send);
}
}
-
-@Injectable()
-export class ReversiChannelService implements MiChannelService<true> {
- public readonly shouldShare = ReversiChannel.shouldShare;
- public readonly requireCredential = ReversiChannel.requireCredential;
- public readonly kind = ReversiChannel.kind;
-
- constructor(
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ReversiChannel {
- return new ReversiChannel(
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index fcfa26c38b..99e0b69023 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -3,28 +3,30 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class RoleTimelineChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline';
public static shouldShare = false;
public static requireCredential = false as const;
private roleId: string;
constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
private noteEntityService: NoteEntityService,
private roleservice: RoleService,
-
- id: string,
- connection: Channel['connection'],
) {
- super(id, connection);
+ super(request);
//this.onNote = this.onNote.bind(this);
}
@@ -60,26 +62,3 @@ class RoleTimelineChannel extends Channel {
this.subscriber.off(`roleTimelineStream:${this.roleId}`, this.onEvent);
}
}
-
-@Injectable()
-export class RoleTimelineChannelService implements MiChannelService<false> {
- public readonly shouldShare = RoleTimelineChannel.shouldShare;
- public readonly requireCredential = RoleTimelineChannel.requireCredential;
- public readonly kind = RoleTimelineChannel.kind;
-
- constructor(
- private noteEntityService: NoteEntityService,
- private roleservice: RoleService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
- return new RoleTimelineChannel(
- this.noteEntityService,
- this.roleservice,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index ec5352d12d..aece5435b0 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -4,21 +4,26 @@
*/
import Xev from 'xev';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
const ev = new Xev();
-class ServerStatsChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class ServerStatsChannel extends Channel {
public readonly chName = 'serverStats';
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(
+ @Inject(REQUEST)
+ request: ChannelRequest,
+ ) {
+ super(request);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -54,22 +59,3 @@ class ServerStatsChannel extends Channel {
ev.removeListener('serverStats', this.onStats);
}
}
-
-@Injectable()
-export class ServerStatsChannelService implements MiChannelService<false> {
- public readonly shouldShare = ServerStatsChannel.shouldShare;
- public readonly requireCredential = ServerStatsChannel.requireCredential;
- public readonly kind = ServerStatsChannel.kind;
-
- constructor(
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): ServerStatsChannel {
- return new ServerStatsChannel(
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 5bfd8fa68c..2f7345e150 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -11,9 +11,11 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
-import Channel, { type MiChannelService } from '../channel.js';
+import Channel, { type ChannelRequest } from '../channel.js';
+import { REQUEST } from '@nestjs/core';
-class UserListChannel extends Channel {
+@Injectable({ scope: Scope.TRANSIENT })
+export class UserListChannel extends Channel {
public readonly chName = 'userList';
public static shouldShare = false;
public static requireCredential = false as const;
@@ -24,14 +26,18 @@ class UserListChannel extends Channel {
private withRenotes: boolean;
constructor(
+ @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
- private noteEntityService: NoteEntityService,
- id: string,
- connection: Channel['connection'],
+ @Inject(REQUEST)
+ request: ChannelRequest,
+
+ private noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(request);
//this.updateListUsers = this.updateListUsers.bind(this);
//this.onNote = this.onNote.bind(this);
}
@@ -130,32 +136,3 @@ class UserListChannel extends Channel {
clearInterval(this.listUsersClock);
}
}
-
-@Injectable()
-export class UserListChannelService implements MiChannelService<false> {
- public readonly shouldShare = UserListChannel.shouldShare;
- public readonly requireCredential = UserListChannel.requireCredential;
- public readonly kind = UserListChannel.kind;
-
- constructor(
- @Inject(DI.userListsRepository)
- private userListsRepository: UserListsRepository,
-
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
-
- private noteEntityService: NoteEntityService,
- ) {
- }
-
- @bindThis
- public create(id: string, connection: Channel['connection']): UserListChannel {
- return new UserListChannel(
- this.userListsRepository,
- this.userListMembershipsRepository,
- this.noteEntityService,
- id,
- connection,
- );
- }
-}
diff --git a/packages/backend/src/server/file/FileServerDriveHandler.ts b/packages/backend/src/server/file/FileServerDriveHandler.ts
new file mode 100644
index 0000000000..51b527b146
--- /dev/null
+++ b/packages/backend/src/server/file/FileServerDriveHandler.ts
@@ -0,0 +1,116 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as fs from 'node:fs';
+import rename from 'rename';
+import type { Config } from '@/config.js';
+import type { IImageStreamable } from '@/core/ImageProcessingService.js';
+import { contentDisposition } from '@/misc/content-disposition.js';
+import { correctFilename } from '@/misc/correct-filename.js';
+import { isMimeImage } from '@/misc/is-mime-image.js';
+import { VideoProcessingService } from '@/core/VideoProcessingService.js';
+import { attachStreamCleanup, handleRangeRequest, setFileResponseHeaders, getSafeContentType, needsCleanup } from './FileServerUtils.js';
+import type { FileServerFileResolver } from './FileServerFileResolver.js';
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+export class FileServerDriveHandler {
+ constructor(
+ private config: Config,
+ private fileResolver: FileServerFileResolver,
+ private assetsPath: string,
+ private videoProcessingService: VideoProcessingService,
+ ) {}
+
+ public async handle(request: FastifyRequest<{ Params: { key: string } }>, reply: FastifyReply) {
+ const key = request.params.key;
+ const file = await this.fileResolver.resolveFileByAccessKey(key);
+
+ if (file.kind === 'not-found') {
+ reply.code(404);
+ reply.header('Cache-Control', 'max-age=86400');
+ return reply.sendFile('/dummy.png', this.assetsPath);
+ }
+
+ if (file.kind === 'unavailable') {
+ reply.code(204);
+ reply.header('Cache-Control', 'max-age=86400');
+ return;
+ }
+
+ try {
+ if (file.kind === 'remote') {
+ let image: IImageStreamable | null = null;
+
+ if (file.fileRole === 'thumbnail') {
+ if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+
+ const url = new URL(`${this.config.mediaProxy}/static.webp`);
+ url.searchParams.set('url', file.url);
+ url.searchParams.set('static', '1');
+
+ file.cleanup();
+ return await reply.redirect(url.toString(), 301);
+ } else if (file.mime.startsWith('video/')) {
+ const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
+ if (externalThumbnail) {
+ file.cleanup();
+ return await reply.redirect(externalThumbnail, 301);
+ }
+
+ image = await this.videoProcessingService.generateVideoThumbnail(file.path);
+ }
+ }
+
+ if (file.fileRole === 'webpublic') {
+ if (['image/svg+xml'].includes(file.mime)) {
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+
+ const url = new URL(`${this.config.mediaProxy}/svg.webp`);
+ url.searchParams.set('url', file.url);
+
+ file.cleanup();
+ return await reply.redirect(url.toString(), 301);
+ }
+ }
+
+ image ??= {
+ data: handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+
+ attachStreamCleanup(image.data, file.cleanup);
+
+ reply.header('Content-Type', getSafeContentType(image.type));
+ reply.header('Content-Length', file.file.size);
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+ reply.header('Content-Disposition',
+ contentDisposition(
+ 'inline',
+ correctFilename(file.filename, image.ext),
+ ),
+ );
+ return image.data;
+ }
+
+ if (file.fileRole !== 'original') {
+ const filename = rename(file.filename, {
+ suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
+ extname: file.ext ? `.${file.ext}` : '.unknown',
+ }).toString();
+
+ setFileResponseHeaders(reply, { mime: file.mime, filename });
+ return handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path);
+ } else {
+ setFileResponseHeaders(reply, { mime: file.file.type, filename: file.filename, size: file.file.size });
+ return handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path);
+ }
+ } catch (e) {
+ if (file.kind === 'remote') file.cleanup();
+ throw e;
+ }
+ }
+}
diff --git a/packages/backend/src/server/file/FileServerFileResolver.ts b/packages/backend/src/server/file/FileServerFileResolver.ts
new file mode 100644
index 0000000000..687d486efd
--- /dev/null
+++ b/packages/backend/src/server/file/FileServerFileResolver.ts
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as fs from 'node:fs';
+import type { DriveFilesRepository, MiDriveFile } from '@/models/_.js';
+import { createTemp } from '@/misc/create-temp.js';
+import type { DownloadService } from '@/core/DownloadService.js';
+import type { FileInfoService } from '@/core/FileInfoService.js';
+import type { InternalStorageService } from '@/core/InternalStorageService.js';
+
+export type DownloadedFileResult = {
+ kind: 'downloaded';
+ mime: string;
+ ext: string | null;
+ path: string;
+ cleanup: () => void;
+ filename: string;
+};
+
+export type FileResolveResult =
+ | { kind: 'not-found' }
+ | { kind: 'unavailable' }
+ | {
+ kind: 'stored';
+ fileRole: 'thumbnail' | 'webpublic' | 'original';
+ file: MiDriveFile;
+ filename: string;
+ mime: string;
+ ext: string | null;
+ path: string;
+ }
+ | {
+ kind: 'remote';
+ fileRole: 'thumbnail' | 'webpublic' | 'original';
+ file: MiDriveFile;
+ filename: string;
+ url: string;
+ mime: string;
+ ext: string | null;
+ path: string;
+ cleanup: () => void;
+ };
+
+export class FileServerFileResolver {
+ constructor(
+ private driveFilesRepository: DriveFilesRepository,
+ private fileInfoService: FileInfoService,
+ private downloadService: DownloadService,
+ private internalStorageService: InternalStorageService,
+ ) {}
+
+ public async downloadAndDetectTypeFromUrl(url: string): Promise<DownloadedFileResult> {
+ const [path, cleanup] = await createTemp();
+ try {
+ const { filename } = await this.downloadService.downloadUrl(url, path);
+
+ const { mime, ext } = await this.fileInfoService.detectType(path);
+
+ return {
+ kind: 'downloaded',
+ mime, ext,
+ path, cleanup,
+ filename,
+ };
+ } catch (e) {
+ cleanup();
+ throw e;
+ }
+ }
+
+ public async resolveFileByAccessKey(key: string): Promise<FileResolveResult> {
+ // Fetch drive file
+ const file = await this.driveFilesRepository.createQueryBuilder('file')
+ .where('file.accessKey = :accessKey', { accessKey: key })
+ .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
+ .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
+ .getOne();
+
+ if (file == null) return { kind: 'not-found' };
+
+ const isThumbnail = file.thumbnailAccessKey === key;
+ const isWebpublic = file.webpublicAccessKey === key;
+
+ if (!file.storedInternal) {
+ if (!(file.isLink && file.uri)) return { kind: 'unavailable' };
+ const result = await this.downloadAndDetectTypeFromUrl(file.uri);
+ const { kind: _kind, ...downloaded } = result;
+ file.size = (await fs.promises.stat(downloaded.path)).size; // DB file.sizeは正確とは限らないので
+ return {
+ kind: 'remote',
+ ...downloaded,
+ url: file.uri,
+ fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
+ file,
+ filename: file.name,
+ };
+ }
+
+ const path = this.internalStorageService.resolvePath(key);
+
+ if (isThumbnail || isWebpublic) {
+ const { mime, ext } = await this.fileInfoService.detectType(path);
+ return {
+ kind: 'stored',
+ fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
+ file,
+ filename: file.name,
+ mime, ext,
+ path,
+ };
+ }
+
+ return {
+ kind: 'stored',
+ fileRole: 'original',
+ file,
+ filename: file.name,
+ // 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
+ mime: this.fileInfoService.fixMime(file.type),
+ ext: null,
+ path,
+ };
+ }
+}
diff --git a/packages/backend/src/server/file/FileServerProxyHandler.ts b/packages/backend/src/server/file/FileServerProxyHandler.ts
new file mode 100644
index 0000000000..41e8e47ba5
--- /dev/null
+++ b/packages/backend/src/server/file/FileServerProxyHandler.ts
@@ -0,0 +1,272 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as fs from 'node:fs';
+import sharp from 'sharp';
+import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
+import type { Config } from '@/config.js';
+import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
+import { StatusError } from '@/misc/status-error.js';
+import { contentDisposition } from '@/misc/content-disposition.js';
+import { correctFilename } from '@/misc/correct-filename.js';
+import { isMimeImage } from '@/misc/is-mime-image.js';
+import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
+import { createRangeStream, attachStreamCleanup, needsCleanup } from './FileServerUtils.js';
+import type { DownloadedFileResult, FileResolveResult, FileServerFileResolver } from './FileServerFileResolver.js';
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+type ProxySource = DownloadedFileResult | FileResolveResult;
+type CleanupableFile = ProxySource & { cleanup: () => void };
+type AvailableFile = Exclude<ProxySource, { kind: 'not-found' | 'unavailable' }>;
+type ProxyQuery = {
+ emoji?: string;
+ avatar?: string;
+ static?: string;
+ preview?: string;
+ badge?: string;
+ origin?: string;
+ url?: string;
+};
+
+export class FileServerProxyHandler {
+ constructor(
+ private config: Config,
+ private fileResolver: FileServerFileResolver,
+ private assetsPath: string,
+ private imageProcessingService: ImageProcessingService,
+ ) {}
+
+ public async handle(request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>, reply: FastifyReply) {
+ const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
+
+ if (typeof url !== 'string') {
+ reply.code(400);
+ return;
+ }
+
+ // アバタークロップなど、どうしてもオリジンである必要がある場合
+ const mustOrigin = 'origin' in request.query;
+
+ if (this.config.externalMediaProxyEnabled && !mustOrigin) {
+ return await this.redirectToExternalProxy(request, reply);
+ }
+
+ this.validateUserAgent(request);
+
+ // Create temp file
+ const file = await this.getStreamAndTypeFromUrl(url);
+ if (file.kind === 'not-found') {
+ reply.code(404);
+ reply.header('Cache-Control', 'max-age=86400');
+ return reply.sendFile('/dummy.png', this.assetsPath);
+ }
+
+ if (file.kind === 'unavailable') {
+ reply.code(204);
+ reply.header('Cache-Control', 'max-age=86400');
+ return;
+ }
+
+ try {
+ const image = await this.processImage(file, request, reply);
+
+ if (needsCleanup(file)) {
+ attachStreamCleanup(image.data, file.cleanup);
+ }
+
+ reply.header('Content-Type', image.type);
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+ reply.header('Content-Disposition',
+ contentDisposition(
+ 'inline',
+ correctFilename(file.filename, image.ext),
+ ),
+ );
+ return image.data;
+ } catch (e) {
+ if (needsCleanup(file)) file.cleanup();
+ throw e;
+ }
+ }
+
+ /**
+ * 外部メディアプロキシにリダイレクトする
+ */
+ private async redirectToExternalProxy(
+ request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>,
+ reply: FastifyReply,
+ ) {
+ reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
+
+ const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
+
+ for (const [key, value] of Object.entries(request.query)) {
+ url.searchParams.append(key, value);
+ }
+
+ return reply.redirect(url.toString(), 301);
+ }
+
+ /**
+ * User-Agent を検証する
+ */
+ private validateUserAgent(request: FastifyRequest): void {
+ if (!request.headers['user-agent']) {
+ throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
+ }
+ if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
+ throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
+ }
+ }
+
+ /**
+ * 画像を処理してストリーム可能な形式に変換する
+ */
+ private async processImage(
+ file: AvailableFile,
+ request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>,
+ reply: FastifyReply,
+ ): Promise<IImageStreamable> {
+ const query = request.query;
+
+ const requiresImageConversion = 'emoji' in query || 'avatar' in query || 'static' in query || 'preview' in query || 'badge' in query;
+ const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
+ if (requiresImageConversion && !isConvertibleImage) {
+ throw new StatusError('Unexpected mime', 404);
+ }
+
+ if ('emoji' in query || 'avatar' in query) {
+ return this.processEmojiOrAvatar(file, query);
+ }
+
+ if ('static' in query) {
+ return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
+ }
+
+ if ('preview' in query) {
+ return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
+ }
+
+ if ('badge' in query) {
+ return this.processBadge(file);
+ }
+
+ if (file.mime === 'image/svg+xml') {
+ return this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
+ }
+
+ if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
+ throw new StatusError('Rejected type', 403, 'Rejected type');
+ }
+
+ return this.createDefaultStream(file, request, reply);
+ }
+
+ /**
+ * 絵文字またはアバター用の画像を処理する
+ */
+ private async processEmojiOrAvatar(
+ file: AvailableFile,
+ query: Pick<ProxyQuery, 'emoji' | 'avatar' | 'static'>,
+ ): Promise<IImageStreamable> {
+ const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
+ if (!isAnimationConvertibleImage && !('static' in query)) {
+ return {
+ data: fs.createReadStream(file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+ }
+
+ const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in query) }))
+ .resize({
+ height: 'emoji' in query ? 128 : 320,
+ withoutEnlargement: true,
+ })
+ .webp(webpDefault);
+
+ return {
+ data,
+ ext: 'webp',
+ type: 'image/webp',
+ };
+ }
+
+ /**
+ * バッジ用の画像を処理する
+ */
+ private async processBadge(file: AvailableFile): Promise<IImageStreamable> {
+ const mask = (await sharpBmp(file.path, file.mime))
+ .resize(96, 96, {
+ fit: 'contain',
+ position: 'centre',
+ withoutEnlargement: false,
+ })
+ .greyscale()
+ .normalise()
+ .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
+ .flatten({ background: '#000' })
+ .toColorspace('b-w');
+
+ const stats = await mask.clone().stats();
+
+ if (stats.entropy < 0.1) {
+ throw new StatusError('Skip to provide badge', 404);
+ }
+
+ const data = sharp({
+ create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
+ })
+ .pipelineColorspace('b-w')
+ .boolean(await mask.png().toBuffer(), 'eor');
+
+ return {
+ data: await data.png().toBuffer(),
+ ext: 'png',
+ type: 'image/png',
+ };
+ }
+
+ /**
+ * デフォルトのストリームを作成する(Range リクエスト対応)
+ */
+ private createDefaultStream(
+ file: AvailableFile,
+ request: FastifyRequest,
+ reply: FastifyReply,
+ ): IImageStreamable {
+ if (request.headers.range && 'file' in file && file.file.size > 0) {
+ const { stream, start, end, chunksize } = createRangeStream(request.headers.range as string, file.file.size, file.path);
+
+ reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
+ reply.header('Accept-Ranges', 'bytes');
+ reply.header('Content-Length', chunksize);
+ reply.code(206);
+
+ return {
+ data: stream,
+ ext: file.ext,
+ type: file.mime,
+ };
+ }
+
+ return {
+ data: fs.createReadStream(file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+ }
+
+ private async getStreamAndTypeFromUrl(url: string): Promise<ProxySource> {
+ if (url.startsWith(`${this.config.url}/files/`)) {
+ const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
+ if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
+
+ return await this.fileResolver.resolveFileByAccessKey(key);
+ }
+
+ return await this.fileResolver.downloadAndDetectTypeFromUrl(url);
+ }
+}
diff --git a/packages/backend/src/server/file/FileServerUtils.ts b/packages/backend/src/server/file/FileServerUtils.ts
new file mode 100644
index 0000000000..c5995a2cca
--- /dev/null
+++ b/packages/backend/src/server/file/FileServerUtils.ts
@@ -0,0 +1,107 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as fs from 'node:fs';
+import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
+import { contentDisposition } from '@/misc/content-disposition.js';
+import type { IImageStreamable } from '@/core/ImageProcessingService.js';
+import type { FastifyReply } from 'fastify';
+
+export type RangeStream = {
+ stream: fs.ReadStream;
+ start: number;
+ end: number;
+ chunksize: number;
+};
+
+/**
+ * Range リクエストに対応したストリームを作成する
+ */
+export function createRangeStream(rangeHeader: string, size: number, path: string): RangeStream {
+ const parts = rangeHeader.replace(/bytes=/, '').split('-');
+ const start = parseInt(parts[0], 10);
+ let end = parts[1] ? parseInt(parts[1], 10) : size - 1;
+ if (end > size) {
+ end = size - 1;
+ }
+ const chunksize = end - start + 1;
+
+ return {
+ stream: fs.createReadStream(path, { start, end }),
+ start,
+ end,
+ chunksize,
+ };
+}
+
+/**
+ * ストリームにcleanupハンドラを設定する
+ * ストリームでない場合は即座にcleanupを実行する
+ */
+export function attachStreamCleanup(data: IImageStreamable['data'], cleanup: () => void): void {
+ if ('pipe' in data && typeof data.pipe === 'function') {
+ data.on('end', cleanup);
+ data.on('close', cleanup);
+ } else {
+ cleanup();
+ }
+}
+
+/**
+ * MIME タイプがブラウザセーフかどうかに応じて Content-Type を返す
+ */
+export function getSafeContentType(mime: string): string {
+ return FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream';
+}
+
+/**
+ * Range リクエストを処理してストリームを返す
+ * Range ヘッダーがない場合は通常のストリームを返す
+ */
+export function handleRangeRequest(
+ reply: FastifyReply,
+ rangeHeader: string | undefined,
+ size: number,
+ path: string,
+): fs.ReadStream {
+ if (rangeHeader && size > 0) {
+ const { stream, start, end, chunksize } = createRangeStream(rangeHeader, size, path);
+ reply.header('Content-Range', `bytes ${start}-${end}/${size}`);
+ reply.header('Accept-Ranges', 'bytes');
+ reply.header('Content-Length', chunksize);
+ reply.code(206);
+ return stream;
+ }
+ return fs.createReadStream(path);
+}
+
+export type FileResponseOptions = {
+ mime: string;
+ filename: string;
+ size?: number;
+ cacheControl?: string;
+};
+
+/**
+ * ファイルレスポンス用の共通ヘッダーを設定する
+ */
+export function setFileResponseHeaders(
+ reply: FastifyReply,
+ options: FileResponseOptions,
+): void {
+ reply.header('Content-Type', getSafeContentType(options.mime));
+ reply.header('Cache-Control', options.cacheControl ?? 'max-age=31536000, immutable');
+ reply.header('Content-Disposition', contentDisposition('inline', options.filename));
+ if (options.size !== undefined) {
+ reply.header('Content-Length', options.size);
+ }
+}
+
+/**
+ * cleanup が必要なファイルかどうかを判定する型ガード
+ */
+export function needsCleanup<T extends { kind?: string; cleanup?: () => void }>(file: T): file is T & { cleanup: () => void } {
+ return 'cleanup' in file && typeof file.cleanup === 'function';
+}
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index d2391c43ab..840c34b806 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -123,41 +123,86 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str
return { name, logo };
}
-// https://indieauth.spec.indieweb.org/#client-information-discovery
-// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
-// and if there is an [h-app] with a url property matching the client_id URL,
-// then it should use the name and icon and display them on the authorization prompt."
-// (But we don't display any icon for now)
-// https://indieauth.spec.indieweb.org/#redirect-url
-// "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
-// of redirect_uri at the client_id URL.
-// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
-// look for an exact match of the given redirect_uri in the request against the list of
-// redirect_uris discovered after resolving any relative URLs."
async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise<ClientInformation> {
try {
const res = await httpRequestService.send(id);
+
const redirectUris: string[] = [];
+ let name = id;
+ let logo: string | null = null;
+ // https://indieauth.spec.indieweb.org/#redirect-url
+ // "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
+ // of redirect_uri at the client_id URL.
+ // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
+ // look for an exact match of the given redirect_uri in the request against the list of
+ // redirect_uris discovered after resolving any relative URLs."
const linkHeader = res.headers.get('link');
if (linkHeader) {
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
}
- const text = await res.text();
- const doc = htmlParser.parse(`<div>${text}</div>`);
+ const contentType = res.headers.get('content-type');
+ const mediaType = contentType ? contentType.split(';')[0].trim() : null;
+ if (mediaType === 'application/json') {
+ // Client discovery via JSON document (11 July 2024 spec)
+ // https://indieauth.spec.indieweb.org/#client-metadata
+ // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing
+ // client metadata defined in [RFC7591], the minimum properties for an IndieAuth
+ // client defined below."
- redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
+ const json = await res.json() as {
+ client_id: string;
+ client_name?: string;
+ client_uri: string;
+ logo_uri?: string;
+ redirect_uris?: string[];
+ };
- let name = id;
- let logo: string | null = null;
- if (text) {
- const microformats = parseMicroformats(doc, res.url, id);
- if (typeof microformats.name === 'string') {
- name = microformats.name;
+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
+ // "The authorization server MUST verify that the client_id in the document matches the
+ // client_id of the URL where the document was retrieved."
+ if (json.client_id !== id) {
+ throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request');
+ }
+
+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
+ // "The client_uri MUST be a prefix of the client_id."
+ if (!json.client_uri || !id.startsWith(json.client_uri)) {
+ throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request');
+ }
+
+ if (typeof json.client_name === 'string') {
+ name = json.client_name;
}
- if (typeof microformats.logo === 'string') {
- logo = microformats.logo;
+
+ if (typeof json.logo_uri === 'string') {
+ // Since uri can be relative, resolve it against the document URL
+ logo = new URL(json.logo_uri, res.url).toString();
+ }
+
+ if (Array.isArray(json.redirect_uris)) {
+ redirectUris.push(...json.redirect_uris.filter((uri): uri is string => typeof uri === 'string'));
+ }
+ } else {
+ // Client discovery via HTML microformats (12 February 2022 spec)
+ // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery
+ // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
+ // and if there is an [h-app] with a url property matching the client_id URL,
+ // then it should use the name and icon and display them on the authorization prompt."
+ const text = await res.text();
+ const doc = htmlParser.parse(`<div>${text}</div>`);
+
+ redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
+
+ if (text) {
+ const microformats = parseMicroformats(doc, res.url, id);
+ if (typeof microformats.name === 'string') {
+ name = microformats.name;
+ }
+ if (typeof microformats.logo === 'string') {
+ logo = microformats.logo;
+ }
}
}
@@ -172,6 +217,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
logger.error('Error while fetching client information', { err });
if (err instanceof StatusError) {
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
+ } else if (err instanceof AuthorizationError) {
+ throw err;
} else {
throw new AuthorizationError('Failed to parse client information', 'server_error');
}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index bcea935409..24bc619e79 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -4,8 +4,9 @@
*/
import { randomUUID } from 'node:crypto';
-import { dirname } from 'node:path';
+import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
+import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
@@ -69,13 +70,28 @@ import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
-const staticAssets = `${_dirname}/../../../assets/`;
-const clientAssets = `${_dirname}/../../../../frontend/assets/`;
-const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
-const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
-const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
-const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
-const tarball = `${_dirname}/../../../../../built/tarball/`;
+let rootDir = _dirname;
+// 見つかるまで上に遡る
+while (!fs.existsSync(resolve(rootDir, 'packages'))) {
+ const parentDir = dirname(rootDir);
+ if (parentDir === rootDir) {
+ throw new Error('Cannot find root directory');
+ }
+ rootDir = parentDir;
+}
+
+const backendRootDir = resolve(rootDir, 'packages/backend');
+const frontendRootDir = resolve(rootDir, 'packages/frontend');
+
+const staticAssets = resolve(backendRootDir, 'assets');
+const clientAssets = resolve(frontendRootDir, 'assets');
+const assets = resolve(rootDir, 'built/_frontend_dist_');
+const swAssets = resolve(rootDir, 'built/_sw_dist_');
+const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist');
+const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg');
+const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_');
+const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_');
+const tarball = resolve(rootDir, 'built/tarball');
@Injectable()
export class ClientServerService {
@@ -207,6 +223,7 @@ export class ClientServerService {
//#region vite assets
if (this.config.frontendEmbedManifestExists) {
+ console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`);
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
root: frontendViteOut,
@@ -226,6 +243,7 @@ export class ClientServerService {
done();
});
} else {
+ console.log('[ClientServerService] Proxying to Vite dev server.');
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
const port = (process.env.VITE_PORT ?? '5173');
@@ -297,7 +315,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
- return await reply.sendFile(path, `${_dirname}/../../../../../fluent-emojis/dist/`, {
+ return reply.sendFile(path, fluentEmojisDir, {
maxAge: ms('30 days'),
});
});
@@ -312,7 +330,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
- return await reply.sendFile(path, `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, {
+ return reply.sendFile(path, twemojiDir, {
maxAge: ms('30 days'),
});
});
@@ -326,7 +344,7 @@ export class ClientServerService {
}
const mask = await sharp(
- `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`,
+ `${twemojiDir}/${path.replace('.png', '')}.svg`,
{ density: 1000 },
)
.resize(488, 488)
@@ -854,9 +872,6 @@ export class ClientServerService {
}));
});
- const override = (source: string, target: string, depth = 0) =>
- [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
-
fastify.get('/flush', async (request, reply) => {
let sendHeader = true;