From b75184ec8e3436200bacdcd832e3324702553d20 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 18 Sep 2022 03:27:08 +0900 Subject: なんかもうめっちゃ変えた MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/CreateSystemUserService.ts | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 packages/backend/src/core/CreateSystemUserService.ts (limited to 'packages/backend/src/core/CreateSystemUserService.ts') diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts new file mode 100644 index 0000000000..71f50d7cb3 --- /dev/null +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -0,0 +1,80 @@ +import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import { v4 as uuid } from 'uuid'; +import { IsNull, DataSource } from 'typeorm'; +import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { User } from '@/models/entities/User.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { IdService } from '@/core/IdService.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { DI } from '@/di-symbols.js'; +import generateNativeUserToken from '@/misc/generate-native-user-token.js'; + +@Injectable() +export class CreateSystemUserService { + constructor( + @Inject(DI.db) + private db: DataSource, + + private idService: IdService, + ) { + } + + public async createSystemUser(username: string): Promise { + const password = uuid(); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateNativeUserToken(); + + const keyPair = await genRsaKeyPair(4096); + + let account!: User; + + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + const exist = await transactionalEntityManager.findOneBy(User, { + usernameLower: username.toLowerCase(), + host: IsNull(), + }); + + if (exist) throw new Error('the user is already exists'); + + account = await transactionalEntityManager.insert(User, { + id: this.idService.genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: null, + token: secret, + isAdmin: false, + isLocked: true, + isExplorable: false, + isBot: true, + }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); + + await transactionalEntityManager.insert(UserKeypair, { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + userId: account.id, + }); + + await transactionalEntityManager.insert(UserProfile, { + userId: account.id, + autoAcceptFollowed: false, + password: hash, + }); + + await transactionalEntityManager.insert(UsedUsername, { + createdAt: new Date(), + username: username.toLowerCase(), + }); + }); + + return account; + } +} -- cgit v1.2.3-freya From bbb49457f9fb5d46402e913c92ebf77722cad6ff Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 4 Dec 2022 15:03:09 +0900 Subject: refactor: introduce bindThis decorator to bind this automaticaly --- packages/backend/src/core/AccountUpdateService.ts | 2 + packages/backend/src/core/AiService.ts | 3 + packages/backend/src/core/AntennaService.ts | 6 ++ packages/backend/src/core/AppLockService.ts | 4 + packages/backend/src/core/CaptchaService.ts | 5 ++ .../backend/src/core/CreateNotificationService.ts | 4 + .../backend/src/core/CreateSystemUserService.ts | 2 + packages/backend/src/core/CustomEmojiService.ts | 8 ++ packages/backend/src/core/DeleteAccountService.ts | 2 + packages/backend/src/core/DownloadService.ts | 4 + packages/backend/src/core/DriveService.ts | 11 +++ packages/backend/src/core/EmailService.ts | 3 + .../backend/src/core/FederatedInstanceService.ts | 2 + .../src/core/FetchInstanceMetadataService.ts | 10 +++ packages/backend/src/core/FileInfoService.ts | 11 +++ packages/backend/src/core/GlobalEventService.ts | 3 + packages/backend/src/core/HashtagService.ts | 4 + packages/backend/src/core/HttpRequestService.ts | 5 ++ packages/backend/src/core/IdService.ts | 2 + .../backend/src/core/ImageProcessingService.ts | 7 ++ packages/backend/src/core/InstanceActorService.ts | 2 + .../backend/src/core/InternalStorageService.ts | 6 ++ packages/backend/src/core/LoggerService.ts | 2 + packages/backend/src/core/MessagingService.ts | 7 ++ packages/backend/src/core/MetaService.ts | 7 +- packages/backend/src/core/MfmService.ts | 3 + packages/backend/src/core/ModerationLogService.ts | 2 + packages/backend/src/core/NoteCreateService.ts | 13 +++ packages/backend/src/core/NoteDeleteService.ts | 4 + packages/backend/src/core/NotePiningService.ts | 4 + packages/backend/src/core/NoteReadService.ts | 3 + packages/backend/src/core/NotificationService.ts | 5 ++ packages/backend/src/core/PollService.ts | 3 + packages/backend/src/core/ProxyAccountService.ts | 2 + .../backend/src/core/PushNotificationService.ts | 1 + packages/backend/src/core/QueryService.ts | 10 +++ packages/backend/src/core/QueueService.ts | 20 +++++ packages/backend/src/core/ReactionService.ts | 8 ++ packages/backend/src/core/RelayService.ts | 8 ++ packages/backend/src/core/RemoteLoggerService.ts | 1 + .../backend/src/core/RemoteUserResolveService.ts | 3 + packages/backend/src/core/S3Service.ts | 2 + packages/backend/src/core/SignupService.ts | 2 + .../src/core/TwoFactorAuthenticationService.ts | 4 + packages/backend/src/core/UserBlockingService.ts | 6 ++ packages/backend/src/core/UserCacheService.ts | 5 +- packages/backend/src/core/UserFollowingService.ts | 16 ++++ .../backend/src/core/UserKeypairStoreService.ts | 2 + packages/backend/src/core/UserListService.ts | 2 + packages/backend/src/core/UserMutingService.ts | 2 + packages/backend/src/core/UserSuspendService.ts | 3 + packages/backend/src/core/UtilityService.ts | 6 ++ .../backend/src/core/VideoProcessingService.ts | 2 + packages/backend/src/core/WebfingerService.ts | 3 + packages/backend/src/core/WebhookService.ts | 6 +- .../src/core/activitypub/ApAudienceService.ts | 5 ++ .../src/core/activitypub/ApDbResolverService.ts | 7 ++ .../core/activitypub/ApDeliverManagerService.ts | 8 ++ .../backend/src/core/activitypub/ApInboxService.ts | 28 ++++++ .../src/core/activitypub/ApLoggerService.ts | 1 + .../backend/src/core/activitypub/ApMfmService.ts | 3 + .../src/core/activitypub/ApRendererService.ts | 33 +++++++ .../src/core/activitypub/ApRequestService.ts | 9 ++ .../src/core/activitypub/ApResolverService.ts | 100 +++++++++++---------- .../src/core/activitypub/LdSignatureService.ts | 33 ++++--- .../src/core/activitypub/models/ApImageService.ts | 3 + .../core/activitypub/models/ApMentionService.ts | 3 + .../src/core/activitypub/models/ApNoteService.ts | 6 ++ .../src/core/activitypub/models/ApPersonService.ts | 8 ++ .../core/activitypub/models/ApQuestionService.ts | 3 + .../backend/src/core/chart/ChartLoggerService.ts | 1 + .../src/core/chart/ChartManagementService.ts | 2 + .../backend/src/core/chart/charts/active-users.ts | 3 + .../backend/src/core/chart/charts/ap-request.ts | 4 + packages/backend/src/core/chart/charts/drive.ts | 2 + .../backend/src/core/chart/charts/federation.ts | 3 + packages/backend/src/core/chart/charts/hashtag.ts | 2 + packages/backend/src/core/chart/charts/instance.ts | 8 ++ packages/backend/src/core/chart/charts/notes.ts | 2 + .../src/core/chart/charts/per-user-drive.ts | 2 + .../src/core/chart/charts/per-user-following.ts | 2 + .../src/core/chart/charts/per-user-notes.ts | 2 + .../src/core/chart/charts/per-user-reactions.ts | 2 + .../backend/src/core/chart/charts/test-grouped.ts | 2 + .../src/core/chart/charts/test-intersection.ts | 3 + .../backend/src/core/chart/charts/test-unique.ts | 2 + packages/backend/src/core/chart/charts/test.ts | 3 + packages/backend/src/core/chart/charts/users.ts | 2 + packages/backend/src/core/chart/core.ts | 11 +++ .../core/entities/AbuseUserReportEntityService.ts | 3 + .../src/core/entities/AntennaEntityService.ts | 2 + .../backend/src/core/entities/AppEntityService.ts | 2 + .../src/core/entities/AuthSessionEntityService.ts | 2 + .../src/core/entities/BlockingEntityService.ts | 3 + .../src/core/entities/ChannelEntityService.ts | 2 + .../backend/src/core/entities/ClipEntityService.ts | 3 + .../src/core/entities/DriveFileEntityService.ts | 11 +++ .../src/core/entities/DriveFolderEntityService.ts | 2 + .../src/core/entities/EmojiEntityService.ts | 3 + .../core/entities/FollowRequestEntityService.ts | 2 + .../src/core/entities/FollowingEntityService.ts | 7 ++ .../src/core/entities/GalleryLikeEntityService.ts | 3 + .../src/core/entities/GalleryPostEntityService.ts | 3 + .../src/core/entities/HashtagEntityService.ts | 3 + .../src/core/entities/InstanceEntityService.ts | 3 + .../core/entities/MessagingMessageEntityService.ts | 2 + .../core/entities/ModerationLogEntityService.ts | 3 + .../src/core/entities/MutingEntityService.ts | 3 + .../backend/src/core/entities/NoteEntityService.ts | 8 ++ .../src/core/entities/NoteFavoriteEntityService.ts | 3 + .../src/core/entities/NoteReactionEntityService.ts | 2 + .../src/core/entities/NotificationEntityService.ts | 3 + .../backend/src/core/entities/PageEntityService.ts | 3 + .../src/core/entities/PageLikeEntityService.ts | 3 + .../src/core/entities/SigninEntityService.ts | 2 + .../backend/src/core/entities/UserEntityService.ts | 12 +++ .../src/core/entities/UserGroupEntityService.ts | 2 + .../entities/UserGroupInvitationEntityService.ts | 3 + .../src/core/entities/UserListEntityService.ts | 2 + packages/backend/src/daemons/JanitorService.ts | 3 + packages/backend/src/daemons/QueueStatsService.ts | 3 + packages/backend/src/daemons/ServerStatsService.ts | 3 + packages/backend/src/decorators.ts | 41 +++++++++ packages/backend/src/logger.ts | 8 ++ packages/backend/src/misc/cache.ts | 7 ++ packages/backend/src/misc/i18n.ts | 3 +- packages/backend/src/postgre.ts | 8 ++ .../backend/src/queue/DbQueueProcessorsService.ts | 2 + .../queue/ObjectStorageQueueProcessorsService.ts | 2 + packages/backend/src/queue/QueueLoggerService.ts | 1 + .../backend/src/queue/QueueProcessorService.ts | 2 + .../src/queue/SystemQueueProcessorsService.ts | 2 + .../CheckExpiredMutingsProcessorService.ts | 2 + .../processors/CleanChartsProcessorService.ts | 2 + .../src/queue/processors/CleanProcessorService.ts | 2 + .../processors/CleanRemoteFilesProcessorService.ts | 2 + .../processors/DeleteAccountProcessorService.ts | 2 + .../processors/DeleteDriveFilesProcessorService.ts | 2 + .../queue/processors/DeleteFileProcessorService.ts | 2 + .../queue/processors/DeliverProcessorService.ts | 2 + .../EndedPollNotificationProcessorService.ts | 2 + .../processors/ExportBlockingProcessorService.ts | 2 + .../ExportCustomEmojisProcessorService.ts | 2 + .../processors/ExportFollowingProcessorService.ts | 2 + .../processors/ExportMutingProcessorService.ts | 2 + .../processors/ExportNotesProcessorService.ts | 2 + .../processors/ExportUserListsProcessorService.ts | 2 + .../processors/ImportBlockingProcessorService.ts | 2 + .../ImportCustomEmojisProcessorService.ts | 2 + .../processors/ImportFollowingProcessorService.ts | 2 + .../processors/ImportMutingProcessorService.ts | 2 + .../processors/ImportUserListsProcessorService.ts | 2 + .../src/queue/processors/InboxProcessorService.ts | 2 + .../processors/ResyncChartsProcessorService.ts | 2 + .../queue/processors/TickChartsProcessorService.ts | 2 + .../processors/WebhookDeliverProcessorService.ts | 2 + .../backend/src/server/ActivityPubServerService.ts | 12 ++- packages/backend/src/server/FileServerService.ts | 6 +- .../backend/src/server/MediaProxyServerService.ts | 5 +- .../backend/src/server/NodeinfoServerService.ts | 5 +- packages/backend/src/server/ServerService.ts | 2 + .../backend/src/server/WellKnownServerService.ts | 4 +- packages/backend/src/server/api/ApiCallService.ts | 7 ++ .../backend/src/server/api/ApiLoggerService.ts | 1 + .../backend/src/server/api/ApiServerService.ts | 4 +- .../backend/src/server/api/AuthenticateService.ts | 2 + packages/backend/src/server/api/GetterService.ts | 5 ++ .../backend/src/server/api/RateLimiterService.ts | 2 + .../backend/src/server/api/SigninApiService.ts | 2 + packages/backend/src/server/api/SigninService.ts | 2 + .../backend/src/server/api/SignupApiService.ts | 3 + .../src/server/api/StreamingApiServerService.ts | 2 + .../src/server/api/endpoints/admin/suspend-user.ts | 3 + .../backend/src/server/api/endpoints/ap/show.ts | 3 + .../server/api/integration/DiscordServerService.ts | 6 +- .../server/api/integration/GithubServerService.ts | 6 +- .../server/api/integration/TwitterServerService.ts | 6 +- .../src/server/api/stream/ChannelsService.ts | 2 + packages/backend/src/server/api/stream/channel.ts | 2 + .../src/server/api/stream/channels/admin.ts | 3 + .../src/server/api/stream/channels/antenna.ts | 7 +- .../src/server/api/stream/channels/channel.ts | 11 ++- .../src/server/api/stream/channels/drive.ts | 3 + .../server/api/stream/channels/global-timeline.ts | 7 +- .../src/server/api/stream/channels/hashtag.ts | 7 +- .../server/api/stream/channels/home-timeline.ts | 7 +- .../server/api/stream/channels/hybrid-timeline.ts | 7 +- .../server/api/stream/channels/local-timeline.ts | 7 +- .../backend/src/server/api/stream/channels/main.ts | 3 + .../server/api/stream/channels/messaging-index.ts | 3 + .../src/server/api/stream/channels/messaging.ts | 13 ++- .../src/server/api/stream/channels/queue-stats.ts | 10 ++- .../src/server/api/stream/channels/server-stats.ts | 10 ++- .../src/server/api/stream/channels/user-list.ts | 13 ++- packages/backend/src/server/api/stream/index.ts | 32 ++++++- .../backend/src/server/web/ClientServerService.ts | 5 +- packages/backend/src/server/web/FeedService.ts | 2 + .../backend/src/server/web/UrlPreviewService.ts | 3 + packages/backend/test/misc/mock-resolver.ts | 1 + 199 files changed, 969 insertions(+), 96 deletions(-) create mode 100644 packages/backend/src/decorators.ts (limited to 'packages/backend/src/core/CreateSystemUserService.ts') diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index a5ab4fdfce..5f6dfca0ca 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -7,6 +7,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AccountUpdateService { @@ -24,6 +25,7 @@ export class AccountUpdateService { ) { } + @bindThis public async publishToFollowers(userId: User['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 15084b8ff1..aee9504c85 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -12,6 +12,7 @@ const _dirname = dirname(_filename); const REQUIRED_CPU_FLAGS = ['avx2', 'fma']; let isSupportedCpu: undefined | boolean = undefined; +import { bindThis } from '@/decorators.js'; @Injectable() export class AiService { @@ -23,6 +24,7 @@ export class AiService { ) { } + @bindThis public async detectSensitive(path: string): Promise { try { if (isSupportedCpu === undefined) { @@ -53,6 +55,7 @@ export class AiService { } } + @bindThis private async getCpuFlags(): Promise { const str = await si.cpuFlags(); return str.split(/\s+/); diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 8046ba5311..9c120c993d 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AntennaService implements OnApplicationShutdown { @@ -56,10 +57,12 @@ export class AntennaService implements OnApplicationShutdown { this.redisSubscriber.on('message', this.onRedisMessage); } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onRedisMessage); } + @bindThis private async onRedisMessage(_: string, data: string): Promise { const obj = JSON.parse(data); @@ -81,6 +84,7 @@ export class AntennaService implements OnApplicationShutdown { } } + @bindThis public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise { // 通知しない設定になっているか、自分自身の投稿なら既読にする const read = !antenna.notify || (antenna.userId === noteUser.id); @@ -133,6 +137,7 @@ export class AntennaService implements OnApplicationShutdown { /** * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい */ + @bindThis public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { if (note.visibility === 'specified') return false; @@ -217,6 +222,7 @@ export class AntennaService implements OnApplicationShutdown { return true; } + @bindThis public async getAntennas() { if (!this.antennasFetched) { this.antennas = await this.antennasRepository.find(); diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts index 04b3d8b112..1f512b5790 100644 --- a/packages/backend/src/core/AppLockService.ts +++ b/packages/backend/src/core/AppLockService.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; * Retry delay (ms) for lock acquisition */ const retryDelay = 100; +import { bindThis } from '@/decorators.js'; @Injectable() export class AppLockService { @@ -26,14 +27,17 @@ export class AppLockService { * @param timeout Lock timeout (ms), The timeout releases previous lock. * @returns Unlock function */ + @bindThis public getApLock(uri: string, timeout = 30 * 1000): Promise<() => void> { return this.lock(`ap-object:${uri}`, timeout); } + @bindThis public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> { return this.lock(`instance:${host}`, timeout); } + @bindThis public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { return this.lock(`chart-insert:${lockKey}`, timeout); } diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index b60271812c..0207cf58a0 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -3,6 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { bindThis } from '@/decorators.js'; type CaptchaResponse = { success: boolean; @@ -19,6 +20,7 @@ export class CaptchaService { ) { } + @bindThis private async getCaptchaResponse(url: string, secret: string, response: string): Promise { const params = new URLSearchParams({ secret, @@ -45,6 +47,7 @@ export class CaptchaService { return await res.json() as CaptchaResponse; } + @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { throw 'recaptcha-failed: no response provided'; @@ -60,6 +63,7 @@ export class CaptchaService { } } + @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { throw 'hcaptcha-failed: no response provided'; @@ -75,6 +79,7 @@ export class CaptchaService { } } + @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise { if (response == null) { throw 'turnstile-failed: no response provided'; diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index 504661c3bd..f376b7b9cf 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -7,6 +7,7 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CreateNotificationService { @@ -30,6 +31,7 @@ export class CreateNotificationService { ) { } + @bindThis public async createNotification( notifieeId: User['id'], type: Notification['type'], @@ -90,6 +92,7 @@ export class CreateNotificationService { // TODO: locale ファイルをクライアント用とサーバー用で分けたい + @bindThis private async emailNotificationFollow(userId: User['id'], follower: User) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); @@ -101,6 +104,7 @@ export class CreateNotificationService { */ } + @bindThis private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 71f50d7cb3..1e753f65cc 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -10,6 +10,7 @@ import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { DI } from '@/di-symbols.js'; import generateNativeUserToken from '@/misc/generate-native-user-token.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CreateSystemUserService { @@ -21,6 +22,7 @@ export class CreateSystemUserService { ) { } + @bindThis public async createSystemUser(username: string): Promise { const password = uuid(); diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 3319f3efa8..36f88fd743 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -20,6 +20,7 @@ type PopulatedEmoji = { name: string; url: string; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class CustomEmojiService { @@ -43,6 +44,7 @@ export class CustomEmojiService { this.cache = new Cache(1000 * 60 * 60 * 12); } + @bindThis public async add(data: { driveFile: DriveFile; name: string; @@ -67,6 +69,7 @@ export class CustomEmojiService { return emoji; } + @bindThis private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { // クエリに使うホスト let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) @@ -79,6 +82,7 @@ export class CustomEmojiService { return host; } + @bindThis private parseEmojiStr(emojiName: string, noteUserHost: string | null) { const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); if (!match) return { name: null, host: null }; @@ -97,6 +101,7 @@ export class CustomEmojiService { * @param noteUserHost ノートやユーザープロフィールの所有者のホスト * @returns 絵文字情報, nullは未マッチを意味する */ + @bindThis public async populateEmoji(emojiName: string, noteUserHost: string | null): Promise { const { name, host } = this.parseEmojiStr(emojiName, noteUserHost); if (name == null) return null; @@ -123,11 +128,13 @@ export class CustomEmojiService { /** * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される) */ + @bindThis public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise { const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost))); return emojis.filter((x): x is PopulatedEmoji => x != null); } + @bindThis public aggregateNoteEmojis(notes: Note[]) { let emojis: { name: string | null; host: string | null; }[] = []; for (const note of notes) { @@ -154,6 +161,7 @@ export class CustomEmojiService { /** * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します */ + @bindThis public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null); const emojisQuery: any[] = []; diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 53d48c450b..e42c738707 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -4,6 +4,7 @@ import { QueueService } from '@/core/QueueService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteAccountService { @@ -17,6 +18,7 @@ export class DeleteAccountService { ) { } + @bindThis public async deleteAccount(user: { id: string; host: string | null; diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 25965b7ac4..9097bb08e0 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; const pipeline = util.promisify(stream.pipeline); +import { bindThis } from '@/decorators.js'; @Injectable() export class DownloadService { @@ -30,6 +31,7 @@ export class DownloadService { this.logger = this.loggerService.getLogger('download'); } + @bindThis public async downloadUrl(url: string, path: string): Promise { this.logger.info(`Downloading ${chalk.cyan(url)} ...`); @@ -94,6 +96,7 @@ export class DownloadService { this.logger.succ(`Download finished: ${chalk.cyan(url)}`); } + @bindThis public async downloadTextFile(url: string): Promise { // Create temp file const [path, cleanup] = await createTemp(); @@ -112,6 +115,7 @@ export class DownloadService { } } + @bindThis private isPrivateIp(ip: string): boolean { for (const net of this.config.allowedPrivateNetworks ?? []) { const cidr = new IPCIDR(net); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 1d2ba5df8c..b83047dbc2 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -71,6 +71,7 @@ type UploadFromUrlArgs = { requestIp?: string | null; requestHeaders?: Record | null; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class DriveService { @@ -122,6 +123,7 @@ export class DriveService { * @param hash Hash for original * @param size Size for original */ + @bindThis private async save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); @@ -242,6 +244,7 @@ export class DriveService { * @param type Content-Type for original * @param generateWeb Generate webpublic or not */ + @bindThis public async generateAlts(path: string, type: string, generateWeb: boolean) { if (type.startsWith('video/')) { try { @@ -345,6 +348,7 @@ export class DriveService { /** * Upload to ObjectStorage */ + @bindThis private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; @@ -372,6 +376,7 @@ export class DriveService { if (result) this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); } + @bindThis private async deleteOldFile(user: IRemoteUser) { const q = this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) @@ -398,6 +403,7 @@ export class DriveService { * Add file to drive * */ + @bindThis public async addFile({ user, path, @@ -601,6 +607,7 @@ export class DriveService { return file; } + @bindThis public async deleteFile(file: DriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -627,6 +634,7 @@ export class DriveService { this.deletePostProcess(file, isExpired); } + @bindThis public async deleteFileSync(file: DriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -657,6 +665,7 @@ export class DriveService { this.deletePostProcess(file, isExpired); } + @bindThis private async deletePostProcess(file: DriveFile, isExpired = false) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { @@ -683,6 +692,7 @@ export class DriveService { } } + @bindThis public async deleteObjectStorageFile(key: string) { const meta = await this.metaService.fetch(); @@ -694,6 +704,7 @@ export class DriveService { }).promise(); } + @bindThis public async uploadFromUrl({ url, user, diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 019b9087cd..59932a5b88 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -7,6 +7,7 @@ import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import type { UserProfilesRepository } from '@/models/index.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class EmailService { @@ -25,6 +26,7 @@ export class EmailService { this.logger = this.loggerService.getLogger('email'); } + @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { const meta = await this.metaService.fetch(true); @@ -141,6 +143,7 @@ export class EmailService { } } + @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index a05c95a2ae..97745a1168 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -5,6 +5,7 @@ import { Cache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class FederatedInstanceService { @@ -20,6 +21,7 @@ export class FederatedInstanceService { this.cache = new Cache(1000 * 60 * 60); } + @bindThis public async registerOrFetchInstanceDoc(host: string): Promise { host = this.utilityService.toPuny(host); diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index b92ebe6059..4d4945ca7f 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -30,6 +30,7 @@ type NodeInfo = { themeColor?: unknown; }; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class FetchInstanceMetadataService { @@ -46,6 +47,7 @@ export class FetchInstanceMetadataService { this.logger = this.loggerService.getLogger('metadata', 'cyan'); } + @bindThis public async fetchInstanceMetadata(instance: Instance, force = false): Promise { const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host); @@ -105,6 +107,7 @@ export class FetchInstanceMetadataService { } } + @bindThis private async fetchNodeinfo(instance: Instance): Promise { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); @@ -148,6 +151,7 @@ export class FetchInstanceMetadataService { } } + @bindThis private async fetchDom(instance: Instance): Promise { this.logger.info(`Fetching HTML of ${instance.host} ...`); @@ -161,6 +165,7 @@ export class FetchInstanceMetadataService { return doc; } + @bindThis private async fetchManifest(instance: Instance): Promise | null> { const url = 'https://' + instance.host; @@ -171,6 +176,7 @@ export class FetchInstanceMetadataService { return manifest; } + @bindThis private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise { const url = 'https://' + instance.host; @@ -198,6 +204,7 @@ export class FetchInstanceMetadataService { return null; } + @bindThis private async fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { const url = 'https://' + instance.host; @@ -226,6 +233,7 @@ export class FetchInstanceMetadataService { return null; } + @bindThis private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; @@ -237,6 +245,7 @@ export class FetchInstanceMetadataService { return null; } + @bindThis private async getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (info && info.metadata) { if (typeof info.metadata.nodeName === 'string') { @@ -261,6 +270,7 @@ export class FetchInstanceMetadataService { return null; } + @bindThis private async getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (info && info.metadata) { if (typeof info.metadata.nodeDescription === 'string') { diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index fd8a4fdd3a..bea1b3402e 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -14,6 +14,7 @@ import sharp from 'sharp'; import { encode } from 'blurhash'; import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; +import { bindThis } from '@/decorators.js'; const pipeline = util.promisify(stream.pipeline); @@ -42,6 +43,7 @@ const TYPE_SVG = { mime: 'image/svg+xml', ext: 'svg', }; + @Injectable() export class FileInfoService { constructor( @@ -52,6 +54,7 @@ export class FileInfoService { /** * Get file information */ + @bindThis public async getFileInfo(path: string, opts: { skipSensitiveDetection: boolean; sensitiveThreshold?: number; @@ -135,6 +138,7 @@ export class FileInfoService { }; } + @bindThis private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { let sensitive = false; let porn = false; @@ -269,6 +273,7 @@ export class FileInfoService { } } + @bindThis private exists(path: string): Promise { return fs.promises.access(path).then(() => true, () => false); } @@ -276,6 +281,7 @@ export class FileInfoService { /** * Detect MIME Type and extension */ + @bindThis public async detectType(path: string): Promise<{ mime: string; ext: string | null; @@ -312,6 +318,7 @@ export class FileInfoService { /** * Check the file is SVG or not */ + @bindThis public async checkSvg(path: string) { try { const size = await this.getFileSize(path); @@ -325,6 +332,7 @@ export class FileInfoService { /** * Get file size */ + @bindThis public async getFileSize(path: string): Promise { const getStat = util.promisify(fs.stat); return (await getStat(path)).size; @@ -333,6 +341,7 @@ export class FileInfoService { /** * Calculate MD5 hash */ + @bindThis private async calcHash(path: string): Promise { const hash = crypto.createHash('md5').setEncoding('hex'); await pipeline(fs.createReadStream(path), hash); @@ -342,6 +351,7 @@ export class FileInfoService { /** * Detect dimensions of image */ + @bindThis private async detectImageSize(path: string): Promise<{ width: number; height: number; @@ -358,6 +368,7 @@ export class FileInfoService { /** * Calculate average color of image */ + @bindThis private getBlurhash(path: string): Promise { return new Promise((resolve, reject) => { sharp(path) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index df0c9b5ccb..90f911027e 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -25,6 +25,7 @@ import type { import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GlobalEventService { @@ -37,6 +38,7 @@ export class GlobalEventService { ) { } + @bindThis private publish(channel: StreamChannels, type: string | null, value?: any): void { const message = type == null ? value : value == null ? { type: type, body: null } : @@ -99,6 +101,7 @@ export class GlobalEventService { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } + @bindThis public publishNotesStream(note: Packed<'Note'>): void { this.publish('notesStream', null, note); } diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 5ca058e9a4..309cfe8c3f 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -7,6 +7,7 @@ import type { Hashtag } from '@/models/entities/Hashtag.js'; import HashtagChart from '@/core/chart/charts/hashtag.js'; import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class HashtagService { @@ -23,12 +24,14 @@ export class HashtagService { ) { } + @bindThis public async updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag); } } + @bindThis public async updateUsertags(user: User, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag, true, true); @@ -39,6 +42,7 @@ export class HashtagService { } } + @bindThis public async updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) { tag = normalizeForSearch(tag); diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 396fefad1c..d998307973 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; +import { bindThis } from '@/decorators.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; @@ -84,6 +85,7 @@ export class HttpRequestService { * @param url URL * @param bypassProxy Allways bypass proxy */ + @bindThis public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) { return url.protocol === 'http:' ? this.http : this.https; @@ -92,6 +94,7 @@ export class HttpRequestService { } } + @bindThis public async getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record): Promise { const res = await this.getResponse({ url, @@ -106,6 +109,7 @@ export class HttpRequestService { return await res.json(); } + @bindThis public async getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record): Promise { const res = await this.getResponse({ url, @@ -120,6 +124,7 @@ export class HttpRequestService { return await res.text(); } + @bindThis public async getResponse(args: { url: string, method: string, diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 997be17937..0e8a7b13ad 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -6,6 +6,7 @@ import { genAid } from '@/misc/id/aid.js'; import { genMeid } from '@/misc/id/meid.js'; import { genMeidg } from '@/misc/id/meidg.js'; import { genObjectId } from '@/misc/id/object-id.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class IdService { @@ -18,6 +19,7 @@ export class IdService { this.metohd = config.id.toLowerCase(); } + @bindThis public genId(date?: Date): string { if (!date || (date > new Date())) date = new Date(); diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 3a50361a42..3a61873044 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -8,6 +8,7 @@ export type IImage = { ext: string | null; type: string; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class ImageProcessingService { @@ -21,10 +22,12 @@ export class ImageProcessingService { * Convert to JPEG * with resize, remove metadata, resolve orientation, stop animation */ + @bindThis public async convertToJpeg(path: string, width: number, height: number): Promise { return this.convertSharpToJpeg(await sharp(path), width, height); } + @bindThis public async convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { const data = await sharp .resize(width, height, { @@ -49,10 +52,12 @@ export class ImageProcessingService { * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ + @bindThis public async convertToWebp(path: string, width: number, height: number, quality = 85): Promise { return this.convertSharpToWebp(await sharp(path), width, height, quality); } + @bindThis public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise { const data = await sharp .resize(width, height, { @@ -76,10 +81,12 @@ export class ImageProcessingService { * Convert to PNG * with resize, remove metadata, resolve orientation, stop animation */ + @bindThis public async convertToPng(path: string, width: number, height: number): Promise { return this.convertSharpToPng(await sharp(path), width, height); } + @bindThis public async convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise { const data = await sharp .resize(width, height, { diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index f35a28147d..abd6685d61 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; const ACTOR_USERNAME = 'instance.actor' as const; +import { bindThis } from '@/decorators.js'; @Injectable() export class InstanceActorService { @@ -21,6 +22,7 @@ export class InstanceActorService { this.cache = new Cache(Infinity); } + @bindThis public async getInstanceActor(): Promise { const cached = this.cache.get(null); if (cached) return cached; diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 6d2a9b2db6..e32cbf645c 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -10,6 +10,7 @@ const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const path = Path.resolve(_dirname, '../../../../files'); +import { bindThis } from '@/decorators.js'; @Injectable() export class InternalStorageService { @@ -19,26 +20,31 @@ export class InternalStorageService { ) { } + @bindThis public resolvePath(key: string) { return Path.resolve(path, key); } + @bindThis public read(key: string) { return fs.createReadStream(this.resolvePath(key)); } + @bindThis public saveFromPath(key: string, srcPath: string) { fs.mkdirSync(path, { recursive: true }); fs.copyFileSync(srcPath, this.resolvePath(key)); return `${this.config.url}/files/${key}`; } + @bindThis public saveFromBuffer(key: string, data: Buffer) { fs.mkdirSync(path, { recursive: true }); fs.writeFileSync(this.resolvePath(key), data); return `${this.config.url}/files/${key}`; } + @bindThis public del(key: string) { fs.unlink(this.resolvePath(key), () => {}); } diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index a3192c0262..4303f3ae22 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -3,6 +3,7 @@ import * as SyslogPro from 'syslog-pro'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class LoggerService { @@ -27,6 +28,7 @@ export class LoggerService { } } + @bindThis public getLogger(domain: string, color?: string | undefined, store?: boolean) { return new Logger(domain, color, store, this.syslogClient); } diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts index 9de28ad8db..f4a1090658 100644 --- a/packages/backend/src/core/MessagingService.ts +++ b/packages/backend/src/core/MessagingService.ts @@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class MessagingService { @@ -46,6 +47,7 @@ export class MessagingService { ) { } + @bindThis public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { const message = { id: this.idService.genId(), @@ -140,11 +142,13 @@ export class MessagingService { return messageObj; } + @bindThis public async deleteMessage(message: MessagingMessage) { await this.messagingMessagesRepository.delete(message.id); this.postDeleteMessage(message); } + @bindThis private async postDeleteMessage(message: MessagingMessage) { if (message.recipientId) { const user = await this.usersRepository.findOneByOrFail({ id: message.userId }); @@ -165,6 +169,7 @@ export class MessagingService { /** * Mark messages as read */ + @bindThis public async readUserMessagingMessage( userId: User['id'], otherpartyId: User['id'], @@ -220,6 +225,7 @@ export class MessagingService { /** * Mark messages as read */ + @bindThis public async readGroupMessagingMessage( userId: User['id'], groupId: UserGroup['id'], @@ -284,6 +290,7 @@ export class MessagingService { } } + @bindThis public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) { messages = toArray(messages).filter(x => x.uri); const contents = messages.map(x => this.apRendererService.renderRead(user, x)); diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index c3d41bfccb..ff05779aee 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -5,6 +5,7 @@ import { DI } from '@/di-symbols.js'; import { Meta } from '@/models/entities/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; @Injectable() export class MetaService implements OnApplicationShutdown { @@ -20,7 +21,7 @@ export class MetaService implements OnApplicationShutdown { private globalEventService: GlobalEventService, ) { - this.onMessage = this.onMessage.bind(this); + //this.onMessage = this.onMessage.bind(this); if (process.env.NODE_ENV !== 'test') { this.intervalId = setInterval(() => { @@ -34,6 +35,7 @@ export class MetaService implements OnApplicationShutdown { this.redisSubscriber.on('message', this.onMessage); } + @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); @@ -50,6 +52,7 @@ export class MetaService implements OnApplicationShutdown { } } + @bindThis public async fetch(noCache = false): Promise { if (!noCache && this.cache) return this.cache; @@ -84,6 +87,7 @@ export class MetaService implements OnApplicationShutdown { }); } + @bindThis public async update(data: Partial): Promise { const updated = await this.db.transaction(async transactionalEntityManager => { const metas = await transactionalEntityManager.find(Meta, { @@ -114,6 +118,7 @@ export class MetaService implements OnApplicationShutdown { return updated; } + @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.intervalId); this.redisSubscriber.off('message', this.onMessage); diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 2e03bf3cc0..d53623baaa 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -14,6 +14,7 @@ const treeAdapter = TreeAdapter.defaultTreeAdapter; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; +import { bindThis } from '@/decorators.js'; @Injectable() export class MfmService { @@ -23,6 +24,7 @@ export class MfmService { ) { } + @bindThis public fromHtml(html: string, hashtagNames?: string[]): string { // some AP servers like Pixelfed use br tags as well as newlines html = html.replace(/\r?\n/gi, '\n'); @@ -228,6 +230,7 @@ export class MfmService { } } + @bindThis public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { if (nodes == null) { return null; diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 81ae322b95..80e8cb9e52 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -3,6 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { ModerationLogsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ModerationLogService { @@ -14,6 +15,7 @@ export class ModerationLogService { ) { } + @bindThis public async insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record) { await this.moderationLogsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index cf1566a5e8..a41df28050 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -64,6 +64,7 @@ class NotificationManager { this.queue = []; } + @bindThis public push(notifiee: ILocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; @@ -83,6 +84,7 @@ class NotificationManager { } } + @bindThis public async deliver() { for (const x of this.queue) { // ミュート情報を取得 @@ -130,6 +132,7 @@ type Option = { url?: string | null; app?: App | null; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteCreateService { @@ -188,6 +191,7 @@ export class NoteCreateService { private instanceChart: InstanceChart, ) {} + @bindThis public async create(user: { id: User['id']; username: User['username']; @@ -307,6 +311,7 @@ export class NoteCreateService { return note; } + @bindThis private async insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { const insert = new Note({ id: this.idService.genId(data.createdAt!), @@ -403,6 +408,7 @@ export class NoteCreateService { } } + @bindThis private async postNoteCreated(note: Note, user: { id: User['id']; username: User['username']; @@ -644,6 +650,7 @@ export class NoteCreateService { this.index(note); } + @bindThis private incRenoteCount(renote: Note) { this.notesRepository.createQueryBuilder().update() .set({ @@ -654,6 +661,7 @@ export class NoteCreateService { .execute(); } + @bindThis private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ @@ -683,10 +691,12 @@ export class NoteCreateService { } } + @bindThis private saveReply(reply: Note, note: Note) { this.notesRepository.increment({ id: reply.id }, 'repliesCount', 1); } + @bindThis private async renderNoteOrRenoteActivity(data: Option, note: Note) { if (data.localOnly) return null; @@ -697,6 +707,7 @@ export class NoteCreateService { return this.apRendererService.renderActivity(content); } + @bindThis private index(note: Note) { if (note.text == null || this.config.elasticsearch == null) return; /* @@ -711,6 +722,7 @@ export class NoteCreateService { });*/ } + @bindThis private incNotesCountOfUser(user: { id: User['id']; }) { this.usersRepository.createQueryBuilder().update() .set({ @@ -721,6 +733,7 @@ export class NoteCreateService { .execute(); } + @bindThis private async extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { if (tokens == null) return []; diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index ce6e755a7e..7331d03552 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -15,6 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteDeleteService { @@ -112,6 +113,7 @@ export class NoteDeleteService { }); } + @bindThis private async findCascadingNotes(note: Note) { const cascadingNotes: Note[] = []; @@ -134,6 +136,7 @@ export class NoteDeleteService { return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users } + @bindThis private async getMentionedRemoteUsers(note: Note) { const where = [] as any[]; @@ -159,6 +162,7 @@ export class NoteDeleteService { }) as IRemoteUser[]; } + @bindThis private async deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index a04b52fe4c..f8997574a7 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -11,6 +11,7 @@ import type { Config } from '@/config.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NotePiningService { @@ -40,6 +41,7 @@ export class NotePiningService { * @param user * @param noteId */ + @bindThis public async addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { // Fetch pinee const note = await this.notesRepository.findOneBy({ @@ -79,6 +81,7 @@ export class NotePiningService { * @param user * @param noteId */ + @bindThis public async removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { // Fetch unpinee const note = await this.notesRepository.findOneBy({ @@ -101,6 +104,7 @@ export class NotePiningService { } } + @bindThis public async deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index e0feaa957d..f70495ff30 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -11,6 +11,7 @@ import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThr import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NotificationService } from './NotificationService.js'; import { AntennaService } from './AntennaService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteReadService { @@ -44,6 +45,7 @@ export class NoteReadService { ) { } + @bindThis public async insertNoteUnread(userId: User['id'], note: Note, params: { // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse isSpecified: boolean; @@ -94,6 +96,7 @@ export class NoteReadService { }, 2000); } + @bindThis public async read( userId: User['id'], notes: (Note | Packed<'Note'>)[], diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 8bbc95b02d..9fef36dd2c 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -8,6 +8,7 @@ import type { Notification } from '@/models/entities/Notification.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from './GlobalEventService.js'; import { PushNotificationService } from './PushNotificationService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NotificationService { @@ -21,6 +22,7 @@ export class NotificationService { ) { } + @bindThis public async readNotification( userId: User['id'], notificationIds: Notification['id'][], @@ -42,6 +44,7 @@ export class NotificationService { else return this.postReadNotifications(userId, notificationIds); } + @bindThis public async readNotificationByQuery( userId: User['id'], query: Record, @@ -55,11 +58,13 @@ export class NotificationService { return this.readNotification(userId, notificationIds); } + @bindThis private postReadAllNotifications(userId: User['id']) { this.globalEventService.publishMainStream(userId, 'readAllNotifications'); return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined); } + @bindThis private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { this.globalEventService.publishMainStream(userId, 'readNotifications', notificationIds); return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 287ce8ada4..3cc9b0cc9b 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -11,6 +11,7 @@ import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class PollService { @@ -40,6 +41,7 @@ export class PollService { ) { } + @bindThis public async vote(user: CacheableUser, note: Note, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); @@ -99,6 +101,7 @@ export class PollService { }); } + @bindThis public async deliverQuestionUpdate(noteId: Note['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) throw new Error('note not found'); diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 4cbdadd029..55b70bfc94 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js'; import type { ILocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ProxyAccountService { @@ -14,6 +15,7 @@ export class ProxyAccountService { ) { } + @bindThis public async fetch(): Promise { const meta = await this.metaService.fetch(); if (meta.proxyAccountId == null) return null; diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 98e0841799..bffb24cf4c 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -37,6 +37,7 @@ function truncateNotification(notification: Packed<'Notification'>): any { return notification; } +import { bindThis } from '@/decorators.js'; @Injectable() export class PushNotificationService { diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 771adeaed5..4cc844ccea 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -4,6 +4,7 @@ import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js'; import type { SelectQueryBuilder } from 'typeorm'; +import { bindThis } from '@/decorators.js'; @Injectable() export class QueryService { @@ -59,6 +60,7 @@ export class QueryService { } // ここでいうBlockedは被Blockedの意 + @bindThis public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') @@ -81,6 +83,7 @@ export class QueryService { q.setParameters(blockingQuery.getParameters()); } + @bindThis public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockeeId') @@ -97,6 +100,7 @@ export class QueryService { q.setParameters(blockedQuery.getParameters()); } + @bindThis public generateChannelQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { if (me == null) { q.andWhere('note.channelId IS NULL'); @@ -118,6 +122,7 @@ export class QueryService { } } + @bindThis public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') .select('muted.noteId') @@ -128,6 +133,7 @@ export class QueryService { q.setParameters(mutedQuery.getParameters()); } + @bindThis public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') .select('threadMuted.threadId') @@ -142,6 +148,7 @@ export class QueryService { q.setParameters(mutedQuery.getParameters()); } + @bindThis public generateMutedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }, exclude?: User): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') @@ -186,6 +193,7 @@ export class QueryService { q.setParameters(mutingInstanceQuery.getParameters()); } + @bindThis public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') @@ -196,6 +204,7 @@ export class QueryService { q.setParameters(mutingQuery.getParameters()); } + @bindThis public generateRepliesQuery(q: SelectQueryBuilder, me?: Pick | null): void { if (me == null) { q.andWhere(new Brackets(qb => { qb @@ -221,6 +230,7 @@ export class QueryService { } } + @bindThis public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index a27d68ee19..7956a3a8f9 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; +import { bindThis } from '@/decorators.js'; @Injectable() export class QueueService { @@ -24,6 +25,7 @@ export class QueueService { @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) {} + @bindThis public deliver(user: ThinUser, content: IActivity | null, to: string | null) { if (content == null) return null; if (to == null) return null; @@ -47,6 +49,7 @@ export class QueueService { }); } + @bindThis public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { const data = { activity: activity, @@ -64,6 +67,7 @@ export class QueueService { }); } + @bindThis public createDeleteDriveFilesJob(user: ThinUser) { return this.dbQueue.add('deleteDriveFiles', { user: user, @@ -73,6 +77,7 @@ export class QueueService { }); } + @bindThis public createExportCustomEmojisJob(user: ThinUser) { return this.dbQueue.add('exportCustomEmojis', { user: user, @@ -82,6 +87,7 @@ export class QueueService { }); } + @bindThis public createExportNotesJob(user: ThinUser) { return this.dbQueue.add('exportNotes', { user: user, @@ -91,6 +97,7 @@ export class QueueService { }); } + @bindThis public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { return this.dbQueue.add('exportFollowing', { user: user, @@ -102,6 +109,7 @@ export class QueueService { }); } + @bindThis public createExportMuteJob(user: ThinUser) { return this.dbQueue.add('exportMuting', { user: user, @@ -111,6 +119,7 @@ export class QueueService { }); } + @bindThis public createExportBlockingJob(user: ThinUser) { return this.dbQueue.add('exportBlocking', { user: user, @@ -120,6 +129,7 @@ export class QueueService { }); } + @bindThis public createExportUserListsJob(user: ThinUser) { return this.dbQueue.add('exportUserLists', { user: user, @@ -129,6 +139,7 @@ export class QueueService { }); } + @bindThis public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importFollowing', { user: user, @@ -139,6 +150,7 @@ export class QueueService { }); } + @bindThis public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importMuting', { user: user, @@ -149,6 +161,7 @@ export class QueueService { }); } + @bindThis public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importBlocking', { user: user, @@ -159,6 +172,7 @@ export class QueueService { }); } + @bindThis public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importUserLists', { user: user, @@ -169,6 +183,7 @@ export class QueueService { }); } + @bindThis public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importCustomEmojis', { user: user, @@ -179,6 +194,7 @@ export class QueueService { }); } + @bindThis public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { return this.dbQueue.add('deleteAccount', { user: user, @@ -189,6 +205,7 @@ export class QueueService { }); } + @bindThis public createDeleteObjectStorageFileJob(key: string) { return this.objectStorageQueue.add('deleteFile', { key: key, @@ -198,6 +215,7 @@ export class QueueService { }); } + @bindThis public createCleanRemoteFilesJob() { return this.objectStorageQueue.add('cleanRemoteFiles', {}, { removeOnComplete: true, @@ -205,6 +223,7 @@ export class QueueService { }); } + @bindThis public webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { const data = { type, @@ -228,6 +247,7 @@ export class QueueService { }); } + @bindThis public destroy() { this.deliverQueue.once('cleaned', (jobs, status) => { //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 7a9724e7dd..b02c990566 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -49,6 +49,7 @@ type DecodedReaction = { */ host?: string | null; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class ReactionService { @@ -81,6 +82,7 @@ export class ReactionService { ) { } + @bindThis public async create(user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) { // Check blocking if (note.userId !== user.id) { @@ -196,6 +198,7 @@ export class ReactionService { //#endregion } + @bindThis public async delete(user: { id: User['id']; host: User['host']; }, note: Note) { // if already unreacted const exist = await this.noteReactionsRepository.findOneBy({ @@ -244,11 +247,13 @@ export class ReactionService { //#endregion } + @bindThis public async getFallbackReaction(): Promise { const meta = await this.metaService.fetch(); return meta.useStarForReactionFallback ? '⭐' : '👍'; } + @bindThis public convertLegacyReactions(reactions: Record) { const _reactions = {} as Record; @@ -279,6 +284,7 @@ export class ReactionService { return _reactions2; } + @bindThis public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise { if (reaction == null) return await this.getFallbackReaction(); @@ -311,6 +317,7 @@ export class ReactionService { return await this.getFallbackReaction(); } + @bindThis public decodeReaction(str: string): DecodedReaction { const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); @@ -332,6 +339,7 @@ export class ReactionService { }; } + @bindThis public convertLegacyReaction(reaction: string): string { reaction = this.decodeReaction(reaction).reaction; if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 7951edddcb..5fb853f25a 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; const ACTOR_USERNAME = 'relay.actor' as const; +import { bindThis } from '@/decorators.js'; @Injectable() export class RelayService { @@ -32,6 +33,7 @@ export class RelayService { this.relaysCache = new Cache(1000 * 60 * 10); } + @bindThis private async getRelayActor(): Promise { const user = await this.usersRepository.findOneBy({ host: IsNull(), @@ -44,6 +46,7 @@ export class RelayService { return created as ILocalUser; } + @bindThis public async addRelay(inbox: string): Promise { const relay = await this.relaysRepository.insert({ id: this.idService.genId(), @@ -59,6 +62,7 @@ export class RelayService { return relay; } + @bindThis public async removeRelay(inbox: string): Promise { const relay = await this.relaysRepository.findOneBy({ inbox, @@ -77,11 +81,13 @@ export class RelayService { await this.relaysRepository.delete(relay.id); } + @bindThis public async listRelay(): Promise { const relays = await this.relaysRepository.find(); return relays; } + @bindThis public async relayAccepted(id: string): Promise { const result = await this.relaysRepository.update(id, { status: 'accepted', @@ -90,6 +96,7 @@ export class RelayService { return JSON.stringify(result); } + @bindThis public async relayRejected(id: string): Promise { const result = await this.relaysRepository.update(id, { status: 'rejected', @@ -98,6 +105,7 @@ export class RelayService { return JSON.stringify(result); } + @bindThis public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { if (activity == null) return; diff --git a/packages/backend/src/core/RemoteLoggerService.ts b/packages/backend/src/core/RemoteLoggerService.ts index 68246466c8..0ea5d7b42f 100644 --- a/packages/backend/src/core/RemoteLoggerService.ts +++ b/packages/backend/src/core/RemoteLoggerService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class RemoteLoggerService { diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 809b50f6e9..dde4098624 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -11,6 +11,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { WebfingerService } from '@/core/WebfingerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class RemoteUserResolveService { @@ -31,6 +32,7 @@ export class RemoteUserResolveService { this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); } + @bindThis public async resolveUser(username: string, host: string | null): Promise { const usernameLower = username.toLowerCase(); @@ -116,6 +118,7 @@ export class RemoteUserResolveService { return user; } + @bindThis private async resolveSelf(acctLower: string) { this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await this.webfingerService.webfinger(acctLower).catch(err => { diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 1374ee06c8..0ce69aaa74 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -5,6 +5,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class S3Service { @@ -16,6 +17,7 @@ export class S3Service { ) { } + @bindThis public getS3(meta: Meta) { const u = meta.objectStorageEndpoint != null ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 1e34d9e4f8..9cf203566d 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -14,6 +14,7 @@ import generateUserToken from '@/misc/generate-native-user-token.js'; import UsersChart from './chart/charts/users.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UtilityService } from './UtilityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SignupService { @@ -37,6 +38,7 @@ export class SignupService { ) { } + @bindThis public async signup(opts: { username: User['username']; password?: string | null; diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts index 54e87ec36f..150047fd22 100644 --- a/packages/backend/src/core/TwoFactorAuthenticationService.ts +++ b/packages/backend/src/core/TwoFactorAuthenticationService.ts @@ -103,6 +103,7 @@ function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { `\n-----END ${type}-----\n` ); } +import { bindThis } from '@/decorators.js'; @Injectable() export class TwoFactorAuthenticationService { @@ -115,6 +116,7 @@ export class TwoFactorAuthenticationService { ) { } + @bindThis public hash(data: Buffer) { return crypto .createHash('sha256') @@ -122,6 +124,7 @@ export class TwoFactorAuthenticationService { .digest(); } + @bindThis public verifySignin({ publicKey, authenticatorData, @@ -159,6 +162,7 @@ export class TwoFactorAuthenticationService { .verify(PEMString(publicKey), signature); } + @bindThis public getProcedures() { return { none: { diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 3399bb510f..d411768dc7 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -14,6 +14,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserBlockingService { @@ -50,6 +51,7 @@ export class UserBlockingService { this.logger = this.loggerService.getLogger('user-block'); } + @bindThis public async block(blocker: User, blockee: User) { await Promise.all([ this.cancelRequest(blocker, blockee), @@ -76,6 +78,7 @@ export class UserBlockingService { } } + @bindThis private async cancelRequest(follower: User, followee: User) { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -126,6 +129,7 @@ export class UserBlockingService { } } + @bindThis private async unFollow(follower: User, followee: User) { const following = await this.followingsRepository.findOneBy({ followerId: follower.id, @@ -167,6 +171,7 @@ export class UserBlockingService { } } + @bindThis private async removeFromList(listOwner: User, user: User) { const userLists = await this.userListsRepository.findBy({ userId: listOwner.id, @@ -180,6 +185,7 @@ export class UserBlockingService { } } + @bindThis public async unblock(blocker: CacheableUser, blockee: CacheableUser) { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 25a600a8da..423c8993e3 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -6,6 +6,7 @@ import type { CacheableLocalUser, CacheableUser, ILocalUser } from '@/models/ent import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserCacheService implements OnApplicationShutdown { @@ -23,7 +24,7 @@ export class UserCacheService implements OnApplicationShutdown { private userEntityService: UserEntityService, ) { - this.onMessage = this.onMessage.bind(this); + //this.onMessage = this.onMessage.bind(this); this.userByIdCache = new Cache(Infinity); this.localUserByNativeTokenCache = new Cache(Infinity); @@ -33,6 +34,7 @@ export class UserCacheService implements OnApplicationShutdown { this.redisSubscriber.on('message', this.onMessage); } + @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); @@ -68,6 +70,7 @@ export class UserCacheService implements OnApplicationShutdown { } } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onMessage); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 2f51e2a9df..a6cb042c7d 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -31,6 +31,7 @@ type Remote = IRemoteUser | { inbox: IRemoteUser['inbox']; }; type Both = Local | Remote; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserFollowingService { @@ -66,6 +67,7 @@ export class UserFollowingService { ) { } + @bindThis public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), @@ -140,6 +142,7 @@ export class UserFollowingService { } } + @bindThis private async insertFollowingDoc( followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] @@ -253,6 +256,7 @@ export class UserFollowingService { } } + @bindThis public async unfollow( follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; @@ -305,6 +309,7 @@ export class UserFollowingService { } } + @bindThis private async decrementFollowing( follower: {id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, @@ -333,6 +338,7 @@ export class UserFollowingService { this.perUserFollowingChart.update(follower, followee, false); } + @bindThis public async createFollowRequest( follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; @@ -396,6 +402,7 @@ export class UserFollowingService { } } + @bindThis public async cancelFollowRequest( followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] @@ -431,6 +438,7 @@ export class UserFollowingService { }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); } + @bindThis public async acceptFollowRequest( followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; @@ -458,6 +466,7 @@ export class UserFollowingService { }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); } + @bindThis public async acceptAllFollowRequests( user: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; @@ -476,6 +485,7 @@ export class UserFollowingService { /** * API following/request/reject */ + @bindThis public async rejectFollowRequest(user: Local, follower: Both): Promise { if (this.userEntityService.isRemoteUser(follower)) { this.deliverReject(user, follower); @@ -491,6 +501,7 @@ export class UserFollowingService { /** * API following/reject */ + @bindThis public async rejectFollow(user: Local, follower: Both): Promise { if (this.userEntityService.isRemoteUser(follower)) { this.deliverReject(user, follower); @@ -506,6 +517,7 @@ export class UserFollowingService { /** * AP Reject/Follow */ + @bindThis public async remoteReject(actor: Remote, follower: Local): Promise { await this.removeFollowRequest(actor, follower); await this.removeFollow(actor, follower); @@ -515,6 +527,7 @@ export class UserFollowingService { /** * Remove follow request record */ + @bindThis private async removeFollowRequest(followee: Both, follower: Both): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -529,6 +542,7 @@ export class UserFollowingService { /** * Remove follow record */ + @bindThis private async removeFollow(followee: Both, follower: Both): Promise { const following = await this.followingsRepository.findOneBy({ followeeId: followee.id, @@ -544,6 +558,7 @@ export class UserFollowingService { /** * Deliver Reject to remote */ + @bindThis private async deliverReject(followee: Local, follower: Remote): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -557,6 +572,7 @@ export class UserFollowingService { /** * Publish unfollow to local */ + @bindThis private async publishUnfollow(followee: Both, follower: Local): Promise { const packedFollowee = await this.userEntityService.pack(followee.id, follower, { detail: true, diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts index 8eca03a10b..1d3cc87c8d 100644 --- a/packages/backend/src/core/UserKeypairStoreService.ts +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -4,6 +4,7 @@ import type { UserKeypairsRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserKeypairStoreService { @@ -16,6 +17,7 @@ export class UserKeypairStoreService { this.cache = new Cache(Infinity); } + @bindThis public async getUserKeypair(userId: User['id']): Promise { return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId })); } diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 1d1ead5a1f..054387ff8e 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -9,6 +9,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserListService { @@ -27,6 +28,7 @@ export class UserListService { ) { } + @bindThis public async push(target: User, list: UserList) { await this.userListJoiningsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 4c09e450c8..3029d02c00 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -5,6 +5,7 @@ import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserMutingService { @@ -21,6 +22,7 @@ export class UserMutingService { ) { } + @bindThis public async mute(user: User, target: User): Promise { await this.mutingsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 02f686bab6..df1664942f 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserSuspendService { @@ -28,6 +29,7 @@ export class UserSuspendService { ) { } + @bindThis public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); @@ -57,6 +59,7 @@ export class UserSuspendService { } } + @bindThis public async doPostUnsuspend(user: User): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 15dd684286..1412e6e9aa 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -3,6 +3,7 @@ import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UtilityService { @@ -12,24 +13,29 @@ export class UtilityService { ) { } + @bindThis public getFullApAccount(username: string, host: string | null): string { return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`; } + @bindThis public isSelfHost(host: string | null): boolean { if (host == null) return true; return this.toPuny(this.config.host) === this.toPuny(host); } + @bindThis public extractDbHost(uri: string): string { const url = new URL(uri); return this.toPuny(url.hostname); } + @bindThis public toPuny(host: string): string { return toASCII(host.toLowerCase()); } + @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; return toASCII(host.toLowerCase()); diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index af4036a291..2807960cb7 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -5,6 +5,7 @@ import type { Config } from '@/config.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { createTempDir } from '@/misc/create-temp.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class VideoProcessingService { @@ -16,6 +17,7 @@ export class VideoProcessingService { ) { } + @bindThis public async generateVideoThumbnail(source: string): Promise { const [dir, cleanup] = await createTempDir(); diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index d2a88be583..2d7afe5c88 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -14,6 +14,7 @@ type IWebFinger = { links: ILink[]; subject: string; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class WebfingerService { @@ -25,12 +26,14 @@ export class WebfingerService { ) { } + @bindThis public async webfinger(query: string): Promise { const url = this.genUrl(query); return await this.httpRequestService.getJson(url, 'application/jrd+json, application/json') as IWebFinger; } + @bindThis private genUrl(query: string): string { if (query.match(/^https?:\/\//)) { const u = new URL(query); diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 1a690314f8..91a39f1359 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -4,6 +4,7 @@ import type { WebhooksRepository } from '@/models/index.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import { DI } from '@/di-symbols.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; @Injectable() export class WebhookService implements OnApplicationShutdown { @@ -17,10 +18,11 @@ export class WebhookService implements OnApplicationShutdown { @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, ) { - this.onMessage = this.onMessage.bind(this); + //this.onMessage = this.onMessage.bind(this); this.redisSubscriber.on('message', this.onMessage); } + @bindThis public async getActiveWebhooks() { if (!this.webhooksFetched) { this.webhooks = await this.webhooksRepository.findBy({ @@ -32,6 +34,7 @@ export class WebhookService implements OnApplicationShutdown { return this.webhooks; } + @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); @@ -64,6 +67,7 @@ export class WebhookService implements OnApplicationShutdown { } } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onMessage); } diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 744017aa3a..64f01644a7 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -4,6 +4,7 @@ import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; +import { bindThis } from '@/decorators.js'; import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { ApObject } from './type.js'; @@ -24,6 +25,7 @@ export class ApAudienceService { ) { } + @bindThis public async parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = this.groupingAudience(getApIds(to), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor); @@ -66,6 +68,7 @@ export class ApAudienceService { }; } + @bindThis private groupingAudience(ids: string[], actor: CacheableRemoteUser) { const groups = { public: [] as string[], @@ -88,6 +91,7 @@ export class ApAudienceService { return groups; } + @bindThis private isPublic(id: string) { return [ 'https://www.w3.org/ns/activitystreams#Public', @@ -96,6 +100,7 @@ export class ApAudienceService { ].includes(id); } + @bindThis private isFollowers(id: string, actor: CacheableRemoteUser) { return ( id === (actor.followersUri ?? `${actor.uri}/followers`) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 77d200c3c8..1f28fb3a07 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -9,6 +9,7 @@ import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import { bindThis } from '@/decorators.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; @@ -57,6 +58,7 @@ export class ApDbResolverService { this.publicKeyByUserIdCache = new Cache(Infinity); } + @bindThis public parseUri(value: string | IObject): UriParseResult { const uri = getApId(value); @@ -82,6 +84,7 @@ export class ApDbResolverService { /** * AP Note => Misskey Note in DB */ + @bindThis public async getNoteFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); @@ -98,6 +101,7 @@ export class ApDbResolverService { } } + @bindThis public async getMessageFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); @@ -117,6 +121,7 @@ export class ApDbResolverService { /** * AP Person => Misskey User in DB */ + @bindThis public async getUserFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); @@ -136,6 +141,7 @@ export class ApDbResolverService { /** * AP KeyId => Misskey User and Key */ + @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey; @@ -161,6 +167,7 @@ export class ApDbResolverService { /** * AP Actor id => Misskey User and Key */ + @bindThis public async getAuthUserFromApId(uri: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey | null; diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 6fc75a0397..256cf12651 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -6,6 +6,7 @@ import type { Config } from '@/config.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; interface IRecipe { type: string; @@ -48,6 +49,7 @@ export class ApDeliverManagerService { * @param activity Activity * @param from Followee */ + @bindThis public async deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { const manager = new DeliverManager( this.userEntityService, @@ -65,6 +67,7 @@ export class ApDeliverManagerService { * @param activity Activity * @param to Target user */ + @bindThis public async deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { const manager = new DeliverManager( this.userEntityService, @@ -77,6 +80,7 @@ export class ApDeliverManagerService { await manager.execute(); } + @bindThis public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { return new DeliverManager( this.userEntityService, @@ -114,6 +118,7 @@ class DeliverManager { /** * Add recipe for followers deliver */ + @bindThis public addFollowersRecipe() { const deliver = { type: 'Followers', @@ -126,6 +131,7 @@ class DeliverManager { * Add recipe for direct deliver * @param to To */ + @bindThis public addDirectRecipe(to: IRemoteUser) { const recipe = { type: 'Direct', @@ -139,6 +145,7 @@ class DeliverManager { * Add recipe * @param recipe Recipe */ + @bindThis public addRecipe(recipe: IRecipe) { this.recipes.push(recipe); } @@ -146,6 +153,7 @@ class DeliverManager { /** * Execute delivers */ + @bindThis public async execute() { if (!this.userEntityService.isLocalUser(this.actor)) return; diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 3da384ec2d..79a917426a 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,6 +32,7 @@ import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate } from './type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApInboxService { @@ -85,6 +86,7 @@ export class ApInboxService { this.logger = this.apLoggerService.logger; } + @bindThis public async performActivity(actor: CacheableRemoteUser, activity: IObject) { if (isCollectionOrOrderedCollection(activity)) { const resolver = this.apResolverService.createResolver(); @@ -112,6 +114,7 @@ export class ApInboxService { } } + @bindThis public async performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; @@ -148,6 +151,7 @@ export class ApInboxService { } } + @bindThis private async follow(actor: CacheableRemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -163,6 +167,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async like(actor: CacheableRemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); @@ -180,6 +185,7 @@ export class ApInboxService { }).then(() => 'ok'); } + @bindThis private async read(actor: CacheableRemoteUser, activity: IRead): Promise { const id = await getApId(activity.object); @@ -202,6 +208,7 @@ export class ApInboxService { return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; } + @bindThis private async accept(actor: CacheableRemoteUser, activity: IAccept): Promise { const uri = activity.id ?? activity; @@ -219,6 +226,7 @@ export class ApInboxService { return `skip: Unknown Accept type: ${getApType(object)}`; } + @bindThis private async acceptFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある @@ -242,6 +250,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async add(actor: CacheableRemoteUser, activity: IAdd): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); @@ -261,6 +270,7 @@ export class ApInboxService { throw new Error(`unknown target: ${activity.target}`); } + @bindThis private async announce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); @@ -271,6 +281,7 @@ export class ApInboxService { this.announceNote(actor, activity, targetUri); } + @bindThis private async announceNote(actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); @@ -330,6 +341,7 @@ export class ApInboxService { } } + @bindThis private async block(actor: CacheableRemoteUser, activity: IBlock): Promise { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず @@ -347,6 +359,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async create(actor: CacheableRemoteUser, activity: ICreate): Promise { const uri = getApId(activity); @@ -382,6 +395,7 @@ export class ApInboxService { } } + @bindThis private async createNote(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); @@ -416,6 +430,7 @@ export class ApInboxService { } } + @bindThis private async delete(actor: CacheableRemoteUser, activity: IDelete): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); @@ -457,6 +472,7 @@ export class ApInboxService { } } + @bindThis private async deleteActor(actor: CacheableRemoteUser, uri: string): Promise { this.logger.info(`Deleting the Actor: ${uri}`); @@ -478,6 +494,7 @@ export class ApInboxService { return `ok: queued ${job.name} ${job.id}`; } + @bindThis private async deleteNote(actor: CacheableRemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); @@ -510,6 +527,7 @@ export class ApInboxService { } } + @bindThis private async flag(actor: CacheableRemoteUser, activity: IFlag): Promise { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する @@ -534,6 +552,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async reject(actor: CacheableRemoteUser, activity: IReject): Promise { const uri = activity.id ?? activity; @@ -551,6 +570,7 @@ export class ApInboxService { return `skip: Unknown Reject type: ${getApType(object)}`; } + @bindThis private async rejectFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある @@ -574,6 +594,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async remove(actor: CacheableRemoteUser, activity: IRemove): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); @@ -593,6 +614,7 @@ export class ApInboxService { throw new Error(`unknown target: ${activity.target}`); } + @bindThis private async undo(actor: CacheableRemoteUser, activity: IUndo): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); @@ -618,6 +640,7 @@ export class ApInboxService { return `skip: unknown object type ${getApType(object)}`; } + @bindThis private async undoAccept(actor: CacheableRemoteUser, activity: IAccept): Promise { const follower = await this.apDbResolverService.getUserFromApId(activity.object); if (follower == null) { @@ -637,6 +660,7 @@ export class ApInboxService { return 'skip: フォローされていない'; } + @bindThis private async undoAnnounce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); @@ -651,6 +675,7 @@ export class ApInboxService { return 'ok: deleted'; } + @bindThis private async undoBlock(actor: CacheableRemoteUser, activity: IBlock): Promise { const blockee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -666,6 +691,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async undoFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { @@ -699,6 +725,7 @@ export class ApInboxService { return 'skip: リクエストもフォローもされていない'; } + @bindThis private async undoLike(actor: CacheableRemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); @@ -713,6 +740,7 @@ export class ApInboxService { return 'ok'; } + @bindThis private async update(actor: CacheableRemoteUser, activity: IUpdate): Promise { if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; diff --git a/packages/backend/src/core/activitypub/ApLoggerService.ts b/packages/backend/src/core/activitypub/ApLoggerService.ts index a742cc42da..b9bf1e4054 100644 --- a/packages/backend/src/core/activitypub/ApLoggerService.ts +++ b/packages/backend/src/core/activitypub/ApLoggerService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApLoggerService { diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index 8804fde64a..6116822f7a 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -6,6 +6,7 @@ import { MfmService } from '@/core/MfmService.js'; import type { Note } from '@/models/entities/Note.js'; import { extractApHashtagObjects } from './models/tag.js'; import type { IObject } from './type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApMfmService { @@ -17,12 +18,14 @@ export class ApMfmService { ) { } + @bindThis public htmlToMfm(html: string, tag?: IObject | IObject[]) { const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); return this.mfmService.fromHtml(html, hashtagNames); } + @bindThis public getNoteHtml(note: Note) { if (!note.text) return ''; return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 38a92567c3..1800840ee9 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -25,6 +25,7 @@ import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IActivity, IObject } from './type.js'; import type { IIdentifier } from './models/identifier.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApRendererService { @@ -59,6 +60,7 @@ export class ApRendererService { ) { } + @bindThis public renderAccept(object: any, user: { id: User['id']; host: null }) { return { type: 'Accept', @@ -67,6 +69,7 @@ export class ApRendererService { }; } + @bindThis public renderAdd(user: ILocalUser, target: any, object: any) { return { type: 'Add', @@ -76,6 +79,7 @@ export class ApRendererService { }; } + @bindThis public renderAnnounce(object: any, note: Note) { const attributedTo = `${this.config.url}/users/${note.userId}`; @@ -108,6 +112,7 @@ export class ApRendererService { * * @param block The block to be rendered. The blockee relation must be loaded. */ + @bindThis public renderBlock(block: Blocking) { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); @@ -121,6 +126,7 @@ export class ApRendererService { }; } + @bindThis public renderCreate(object: any, note: Note) { const activity = { id: `${this.config.url}/notes/${note.id}/activity`, @@ -136,6 +142,7 @@ export class ApRendererService { return activity; } + @bindThis public renderDelete(object: any, user: { id: User['id']; host: null }) { return { type: 'Delete', @@ -145,6 +152,7 @@ export class ApRendererService { }; } + @bindThis public renderDocument(file: DriveFile) { return { type: 'Document', @@ -154,6 +162,7 @@ export class ApRendererService { }; } + @bindThis public renderEmoji(emoji: Emoji) { return { id: `${this.config.url}/emojis/${emoji.name}`, @@ -170,6 +179,7 @@ export class ApRendererService { // to anonymise reporters, the reporting actor must be a system user // object has to be a uri or array of uris + @bindThis public renderFlag(user: ILocalUser, object: [string], content: string) { return { type: 'Flag', @@ -179,6 +189,7 @@ export class ApRendererService { }; } + @bindThis public renderFollowRelay(relay: Relay, relayActor: ILocalUser) { const follow = { id: `${this.config.url}/activities/follow-relay/${relay.id}`, @@ -194,11 +205,13 @@ export class ApRendererService { * Convert (local|remote)(Follower|Followee)ID to URL * @param id Follower|Followee ID */ + @bindThis public async renderFollowUser(id: User['id']) { const user = await this.usersRepository.findOneByOrFail({ id: id }); return this.userEntityService.isLocalUser(user) ? `${this.config.url}/users/${user.id}` : user.uri; } + @bindThis public renderFollow( follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, @@ -214,6 +227,7 @@ export class ApRendererService { return follow; } + @bindThis public renderHashtag(tag: string) { return { type: 'Hashtag', @@ -222,6 +236,7 @@ export class ApRendererService { }; } + @bindThis public renderImage(file: DriveFile) { return { type: 'Image', @@ -231,6 +246,7 @@ export class ApRendererService { }; } + @bindThis public renderKey(user: ILocalUser, key: UserKeypair, postfix?: string) { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, @@ -243,6 +259,7 @@ export class ApRendererService { }; } + @bindThis public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }) { const reaction = noteReaction.reaction; @@ -268,6 +285,7 @@ export class ApRendererService { return object; } + @bindThis public renderMention(mention: User) { return { type: 'Mention', @@ -276,6 +294,7 @@ export class ApRendererService { }; } + @bindThis public async renderNote(note: Note, dive = true, isTalk = false): Promise { const getPromisedFiles = async (ids: string[]) => { if (!ids || ids.length === 0) return []; @@ -420,6 +439,7 @@ export class ApRendererService { }; } + @bindThis public async renderPerson(user: ILocalUser) { const id = `${this.config.url}/users/${user.id}`; const isSystem = !!user.username.match(/\./); @@ -496,6 +516,7 @@ export class ApRendererService { return person; } + @bindThis public async renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { const question = { type: 'Question', @@ -515,6 +536,7 @@ export class ApRendererService { return question; } + @bindThis public renderRead(user: { id: User['id'] }, message: MessagingMessage) { return { type: 'Read', @@ -523,6 +545,7 @@ export class ApRendererService { }; } + @bindThis public renderReject(object: any, user: { id: User['id'] }) { return { type: 'Reject', @@ -531,6 +554,7 @@ export class ApRendererService { }; } + @bindThis public renderRemove(user: { id: User['id'] }, target: any, object: any) { return { type: 'Remove', @@ -540,6 +564,7 @@ export class ApRendererService { }; } + @bindThis public renderTombstone(id: string) { return { id, @@ -547,6 +572,7 @@ export class ApRendererService { }; } + @bindThis public renderUndo(object: any, user: { id: User['id'] }) { if (object == null) return null; const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; @@ -560,6 +586,7 @@ export class ApRendererService { }; } + @bindThis public renderUpdate(object: any, user: { id: User['id'] }) { const activity = { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, @@ -573,6 +600,7 @@ export class ApRendererService { return activity; } + @bindThis public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser) { return { id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, @@ -591,6 +619,7 @@ export class ApRendererService { }; } + @bindThis public renderActivity(x: any): IActivity | null { if (x == null) return null; @@ -632,6 +661,7 @@ export class ApRendererService { }, x); } + @bindThis public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise { const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); @@ -651,6 +681,7 @@ export class ApRendererService { * @param prev URL of prev page (optional) * @param next URL of next page (optional) */ + @bindThis public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { const page = { id, @@ -674,6 +705,7 @@ export class ApRendererService { * @param last URL of last page (optional) * @param orderedItems attached objects (optional) */ + @bindThis public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { const page: any = { id, @@ -688,6 +720,7 @@ export class ApRendererService { return page; } + @bindThis private async getEmojis(names: string[]): Promise { if (names == null || names.length === 0) return []; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index baad46d668..d1edd579fa 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -6,6 +6,7 @@ import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { bindThis } from '@/decorators.js'; type Request = { url: string; @@ -36,6 +37,7 @@ export class ApRequestService { ) { } + @bindThis private createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; @@ -61,6 +63,7 @@ export class ApRequestService { }; } + @bindThis private createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); @@ -84,6 +87,7 @@ export class ApRequestService { }; } + @bindThis private signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed { const signingString = this.genSigningString(request, includeHeaders); const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); @@ -101,6 +105,7 @@ export class ApRequestService { }; } + @bindThis private genSigningString(request: Request, includeHeaders: string[]): string { request.headers = this.lcObjectKey(request.headers); @@ -117,16 +122,19 @@ export class ApRequestService { return results.join('\n'); } + @bindThis private lcObjectKey(src: Record): Record { const dst: Record = {}; for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; return dst; } + @bindThis private objectAssignWithLcKey(a: Record, b: Record): Record { return Object.assign(this.lcObjectKey(a), this.lcObjectKey(b)); } + @bindThis public async signedPost(user: { id: User['id'] }, url: string, object: any) { const body = JSON.stringify(object); @@ -157,6 +165,7 @@ export class ApRequestService { * @param user http-signature user * @param url URL to fetch */ + @bindThis public async signedGet(url: string, user: { id: User['id'] }) { const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bcdb9383d1..e96c84f148 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -7,58 +7,13 @@ import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; -@Injectable() -export class ApResolverService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.pollsRepository) - private pollsRepository: PollsRepository, - - @Inject(DI.noteReactionsRepository) - private noteReactionsRepository: NoteReactionsRepository, - - private utilityService: UtilityService, - private instanceActorService: InstanceActorService, - private metaService: MetaService, - private apRequestService: ApRequestService, - private httpRequestService: HttpRequestService, - private apRendererService: ApRendererService, - private apDbResolverService: ApDbResolverService, - ) { - } - - public createResolver(): Resolver { - return new Resolver( - this.config, - this.usersRepository, - this.notesRepository, - this.pollsRepository, - this.noteReactionsRepository, - this.utilityService, - this.instanceActorService, - this.metaService, - this.apRequestService, - this.httpRequestService, - this.apRendererService, - this.apDbResolverService, - ); - } -} - export class Resolver { private history: Set; private user?: ILocalUser; @@ -76,15 +31,17 @@ export class Resolver { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, - private recursionLimit = 100 + private recursionLimit = 100, ) { this.history = new Set(); } + @bindThis public getHistory(): string[] { return Array.from(this.history); } + @bindThis public async resolveCollection(value: string | IObject): Promise { const collection = typeof value === 'string' ? await this.resolve(value) @@ -97,6 +54,7 @@ export class Resolver { } } + @bindThis public async resolve(value: string | IObject): Promise { if (value == null) { throw new Error('resolvee is null (or undefined)'); @@ -152,6 +110,7 @@ export class Resolver { return object; } + @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); if (!parsed.local) throw new Error('resolveLocal: not local'); @@ -193,3 +152,50 @@ export class Resolver { } } } + +@Injectable() +export class ApResolverService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + private utilityService: UtilityService, + private instanceActorService: InstanceActorService, + private metaService: MetaService, + private apRequestService: ApRequestService, + private httpRequestService: HttpRequestService, + private apRendererService: ApRendererService, + private apDbResolverService: ApDbResolverService, + ) { + } + + @bindThis + public createResolver(): Resolver { + return new Resolver( + this.config, + this.usersRepository, + this.notesRepository, + this.pollsRepository, + this.noteReactionsRepository, + this.utilityService, + this.instanceActorService, + this.metaService, + this.apRequestService, + this.httpRequestService, + this.apRendererService, + this.apDbResolverService, + ); + } +} diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index ea39f15b2b..b71320ed0b 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -2,22 +2,11 @@ import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import fetch from 'node-fetch'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 -@Injectable() -export class LdSignatureService { - constructor( - private httpRequestService: HttpRequestService, - ) { - } - - public use(): LdSignature { - return new LdSignature(this.httpRequestService); - } -} - class LdSignature { public debug = false; public preLoad = true; @@ -28,6 +17,7 @@ class LdSignature { ) { } + @bindThis public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { const options = { type: 'RsaSignature2017', @@ -64,6 +54,7 @@ class LdSignature { }; } + @bindThis public async verifyRsaSignature2017(data: any, publicKey: string): Promise { const toBeSigned = await this.createVerifyData(data, data.signature); const verifier = crypto.createVerify('sha256'); @@ -71,6 +62,7 @@ class LdSignature { return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); } + @bindThis public async createVerifyData(data: any, options: any) { const transformedOptions = { ...options, @@ -90,11 +82,13 @@ class LdSignature { return verifyData; } + @bindThis public async normalize(data: any) { const customLoader = this.getLoader(); return 42; } + @bindThis private getLoader() { return async (url: string): Promise => { if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; @@ -120,6 +114,7 @@ class LdSignature { }; } + @bindThis private async fetchDocument(url: string) { const json = await fetch(url, { headers: { @@ -139,9 +134,23 @@ class LdSignature { return json; } + @bindThis public sha256(data: string): string { const hash = crypto.createHash('sha256'); hash.update(data); return hash.digest('hex'); } } + +@Injectable() +export class LdSignatureService { + constructor( + private httpRequestService: HttpRequestService, + ) { + } + + @bindThis + public use(): LdSignature { + return new LdSignature(this.httpRequestService); + } +} diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 9bf87f19d4..58fcc8cb53 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -11,6 +11,7 @@ import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApImageService { @@ -34,6 +35,7 @@ export class ApImageService { /** * Imageを作成します。 */ + @bindThis public async createImage(actor: CacheableRemoteUser, value: any): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -81,6 +83,7 @@ export class ApImageService { * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ + @bindThis public async resolveImage(actor: CacheableRemoteUser, value: any): Promise { // TODO diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 1275e24c62..41e6c6b14f 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -9,6 +9,7 @@ import { isMention } from '../type.js'; import { ApResolverService, Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; import type { IObject, IApMention } from '../type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApMentionService { @@ -21,6 +22,7 @@ export class ApMentionService { ) { } + @bindThis public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); @@ -32,6 +34,7 @@ export class ApMentionService { return mentionedUsers; } + @bindThis public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { if (tags == null) return []; return toArray(tags).filter(isMention); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 7cf6725a38..e1d93a08b0 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -32,6 +32,7 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApNoteService { @@ -74,6 +75,7 @@ export class ApNoteService { this.logger = this.apLoggerService.logger; } + @bindThis public validateNote(object: any, uri: string) { const expectHost = this.utilityService.extractDbHost(uri); @@ -101,6 +103,7 @@ export class ApNoteService { * * Misskeyに対象のNoteが登録されていればそれを返します。 */ + @bindThis public async fetchNote(object: string | IObject): Promise { return await this.apDbResolverService.getNoteFromApId(object); } @@ -108,6 +111,7 @@ export class ApNoteService { /** * Noteを作成します。 */ + @bindThis public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -313,6 +317,7 @@ export class ApNoteService { * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ + @bindThis public async resolveNote(value: string | IObject, resolver?: Resolver): Promise { const uri = typeof value === 'string' ? value : value.id; if (uri == null) throw new Error('missing uri'); @@ -345,6 +350,7 @@ export class ApNoteService { } } + @bindThis public async extractEmojis(tags: IObject | IObject[], host: string): Promise { host = this.utilityService.toPuny(host); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f9d6f42ef6..d5faf37df2 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -72,6 +72,7 @@ function addService(target: { [x: string]: any }, source: IApPropertyValue) { target[source.name.split(':')[2]] = service(id, username); } } +import { bindThis } from '@/decorators.js'; @Injectable() export class ApPersonService implements OnModuleInit { @@ -161,6 +162,7 @@ export class ApPersonService implements OnModuleInit { * @param x Fetched object * @param uri Fetch target URI */ + @bindThis private validateActor(x: IObject, uri: string): IActor { const expectHost = this.utilityService.toPuny(new URL(uri).hostname); @@ -224,6 +226,7 @@ export class ApPersonService implements OnModuleInit { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ + @bindThis public async fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); @@ -253,6 +256,7 @@ export class ApPersonService implements OnModuleInit { /** * Personを作成します。 */ + @bindThis public async createPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); @@ -402,6 +406,7 @@ export class ApPersonService implements OnModuleInit { * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ + @bindThis public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); @@ -512,6 +517,7 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ + @bindThis public async resolvePerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); @@ -528,6 +534,7 @@ export class ApPersonService implements OnModuleInit { return await this.createPerson(uri, resolver); } + @bindThis public analyzeAttachments(attachments: IObject | IObject[] | undefined) { const fields: { name: string, @@ -551,6 +558,7 @@ export class ApPersonService implements OnModuleInit { return { fields, services }; } + @bindThis public async updateFeatured(userId: User['id'], resolver?: Resolver) { const user = await this.usersRepository.findOneByOrFail({ id: userId }); if (!this.userEntityService.isRemoteUser(user)) return; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 5793b98353..13a2f0fa5c 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -9,6 +9,7 @@ import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IQuestion } from '../type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApQuestionService { @@ -30,6 +31,7 @@ export class ApQuestionService { this.logger = this.apLoggerService.logger; } + @bindThis public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -65,6 +67,7 @@ export class ApQuestionService { * @param uri URI of AP Question object * @returns true if updated */ + @bindThis public async updateQuestion(value: any, resolver?: Resolver) { const uri = typeof value === 'string' ? value : value.id; diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index 544a006ac9..d392c6d595 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ChartLoggerService { diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 6476cd6843..13ee06c6c5 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -13,6 +13,7 @@ import PerUserFollowingChart from './charts/per-user-following.js'; import PerUserDriveChart from './charts/per-user-drive.js'; import ApRequestChart from './charts/ap-request.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ChartManagementService implements OnApplicationShutdown { @@ -49,6 +50,7 @@ export class ChartManagementService implements OnApplicationShutdown { ]; } + @bindThis public async run() { // 20分おきにメモリ情報をDBに書き込み this.saveIntervalId = setInterval(() => { diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts index 40c60910ea..bc0ba25cbb 100644 --- a/packages/backend/src/core/chart/charts/active-users.ts +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/active-users.js'; @@ -36,6 +37,7 @@ export default class ActiveUsersChart extends Chart { return {}; } + @bindThis public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { await this.commit({ 'read': [user.id], @@ -48,6 +50,7 @@ export default class ActiveUsersChart extends Chart { }); } + @bindThis public async write(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { await this.commit({ 'write': [user.id], diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts index 4b91fbbf18..ce377460c8 100644 --- a/packages/backend/src/core/chart/charts/ap-request.ts +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -2,6 +2,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/ap-request.js'; @@ -31,18 +32,21 @@ export default class ApRequestChart extends Chart { return {}; } + @bindThis public async deliverSucc(): Promise { await this.commit({ 'deliverSucceeded': 1, }); } + @bindThis public async deliverFail(): Promise { await this.commit({ 'deliverFailed': 1, }); } + @bindThis public async inbox(): Promise { await this.commit({ 'inboxReceived': 1, diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index 494dfbbe57..da36b944f5 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -3,6 +3,7 @@ import { Not, IsNull, DataSource } from 'typeorm'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/drive.js'; @@ -32,6 +33,7 @@ export default class DriveChart extends Chart { return {}; } + @bindThis public async update(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit(file.userHost === null ? { diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index 21e4cedea3..d9234e8028 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -4,6 +4,7 @@ import type { FollowingsRepository, InstancesRepository } from '@/models/index.j import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/federation.js'; @@ -107,6 +108,7 @@ export default class FederationChart extends Chart { }; } + @bindThis public async deliverd(host: string, succeeded: boolean): Promise { await this.commit(succeeded ? { 'deliveredInstances': [host], @@ -115,6 +117,7 @@ export default class FederationChart extends Chart { }); } + @bindThis public async inbox(host: string): Promise { await this.commit({ 'inboxInstances': [host], diff --git a/packages/backend/src/core/chart/charts/hashtag.ts b/packages/backend/src/core/chart/charts/hashtag.ts index 8b8c795cfd..3899b41363 100644 --- a/packages/backend/src/core/chart/charts/hashtag.ts +++ b/packages/backend/src/core/chart/charts/hashtag.ts @@ -4,6 +4,7 @@ import type { User } from '@/models/entities/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/hashtag.js'; @@ -34,6 +35,7 @@ export default class HashtagChart extends Chart { return {}; } + @bindThis public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise { await this.commit({ 'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [], diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index 2e0f4c7126..8ca88d80e3 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -6,6 +6,7 @@ import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/instance.js'; @@ -68,12 +69,14 @@ export default class InstanceChart extends Chart { return {}; } + @bindThis public async requestReceived(host: string): Promise { await this.commit({ 'requests.received': 1, }, this.utilityService.toPuny(host)); } + @bindThis public async requestSent(host: string, isSucceeded: boolean): Promise { await this.commit({ 'requests.succeeded': isSucceeded ? 1 : 0, @@ -81,6 +84,7 @@ export default class InstanceChart extends Chart { }, this.utilityService.toPuny(host)); } + @bindThis public async newUser(host: string): Promise { await this.commit({ 'users.total': 1, @@ -88,6 +92,7 @@ export default class InstanceChart extends Chart { }, this.utilityService.toPuny(host)); } + @bindThis public async updateNote(host: string, note: Note, isAdditional: boolean): Promise { await this.commit({ 'notes.total': isAdditional ? 1 : -1, @@ -100,6 +105,7 @@ export default class InstanceChart extends Chart { }, this.utilityService.toPuny(host)); } + @bindThis public async updateFollowing(host: string, isAdditional: boolean): Promise { await this.commit({ 'following.total': isAdditional ? 1 : -1, @@ -108,6 +114,7 @@ export default class InstanceChart extends Chart { }, this.utilityService.toPuny(host)); } + @bindThis public async updateFollowers(host: string, isAdditional: boolean): Promise { await this.commit({ 'followers.total': isAdditional ? 1 : -1, @@ -116,6 +123,7 @@ export default class InstanceChart extends Chart { }, this.utilityService.toPuny(host)); } + @bindThis public async updateDrive(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index 2153cfe4b4..23dc248fec 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -4,6 +4,7 @@ import type { NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/notes.js'; @@ -44,6 +45,7 @@ export default class NotesChart extends Chart { return {}; } + @bindThis public async update(note: Note, isAdditional: boolean): Promise { const prefix = note.userHost === null ? 'local' : 'remote'; diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index a44460bb4e..ffba04b041 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -5,6 +5,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-drive.js'; @@ -46,6 +47,7 @@ export default class PerUserDriveChart extends Chart { return {}; } + @bindThis public async update(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts index 5ea08a0872..aea6d44a9a 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -5,6 +5,7 @@ import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { FollowingsRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-following.js'; @@ -55,6 +56,7 @@ export default class PerUserFollowingChart extends Chart { return {}; } + @bindThis public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise { const prefixFollower = this.userEntityService.isLocalUser(follower) ? 'local' : 'remote'; const prefixFollowee = this.userEntityService.isLocalUser(followee) ? 'local' : 'remote'; diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index 5c14309d89..1e2a579dfa 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -5,6 +5,7 @@ import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import type { NotesRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-notes.js'; @@ -43,6 +44,7 @@ export default class PerUserNotesChart extends Chart { return {}; } + @bindThis public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise { await this.commit({ 'total': isAdditional ? 1 : -1, diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts index 4160219720..7bc6d4b521 100644 --- a/packages/backend/src/core/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -5,6 +5,7 @@ import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-reactions.js'; @@ -35,6 +36,7 @@ export default class PerUserReactionsChart extends Chart { return {}; } + @bindThis public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; this.commit({ diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts index bc215f3942..128967bc65 100644 --- a/packages/backend/src/core/chart/charts/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-grouped.js'; import type { KVs } from '../core.js'; @@ -35,6 +36,7 @@ export default class TestGroupedChart extends Chart { return {}; } + @bindThis public async increment(group: string): Promise { if (this.total[group] == null) this.total[group] = 0; diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts index a074a7dded..6b4eed9062 100644 --- a/packages/backend/src/core/chart/charts/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-intersection.js'; import type { KVs } from '../core.js'; @@ -31,12 +32,14 @@ export default class TestIntersectionChart extends Chart { return {}; } + @bindThis public async addA(key: string): Promise { await this.commit({ a: [key], }); } + @bindThis public async addB(key: string): Promise { await this.commit({ b: [key], diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts index 4d3e2f2403..5d2b3f8ab1 100644 --- a/packages/backend/src/core/chart/charts/test-unique.ts +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-unique.js'; import type { KVs } from '../core.js'; @@ -31,6 +32,7 @@ export default class TestUniqueChart extends Chart { return {}; } + @bindThis public async uniqueIncrement(key: string): Promise { await this.commit({ foo: [key], diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts index 72caf79e0f..238351d8b3 100644 --- a/packages/backend/src/core/chart/charts/test.ts +++ b/packages/backend/src/core/chart/charts/test.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { name, schema } from './entities/test.js'; import type { KVs } from '../core.js'; @@ -35,6 +36,7 @@ export default class TestChart extends Chart { return {}; } + @bindThis public async increment(): Promise { this.total++; @@ -44,6 +46,7 @@ export default class TestChart extends Chart { }); } + @bindThis public async decrement(): Promise { this.total--; diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index f0359968eb..7bc3602439 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -5,6 +5,7 @@ import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UsersRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/users.js'; @@ -46,6 +47,7 @@ export default class UsersChart extends Chart { return {}; } + @bindThis public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index cf5aa48884..2092b13b7e 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -8,6 +8,7 @@ import * as nestedProperty from 'nested-property'; import { EntitySchema, LessThan, Between } from 'typeorm'; import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js'; import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import type { Repository, DataSource } from 'typeorm'; const columnPrefix = '___' as const; @@ -249,6 +250,7 @@ export default abstract class Chart { this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); } + @bindThis private convertRawRecord(x: RawRecord): KVs { const kvs = {} as Record; for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns)[]) { @@ -257,6 +259,7 @@ export default abstract class Chart { return kvs as KVs; } + @bindThis private getNewLog(latest: KVs | null): KVs { const log = {} as Record; for (const [k, v] of Object.entries(this.schema) as ([keyof typeof this['schema'], this['schema'][string]])[]) { @@ -269,6 +272,7 @@ export default abstract class Chart { return log as KVs; } + @bindThis private getLatestLog(group: string | null, span: 'hour' | 'day'): Promise | null> { const repository = span === 'hour' ? this.repositoryForHour : @@ -288,6 +292,7 @@ export default abstract class Chart { /** * 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。 */ + @bindThis private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { const [y, m, d, h] = Chart.getCurrentDate(); @@ -380,6 +385,7 @@ export default abstract class Chart { }); } + @bindThis public async save(): Promise { if (this.buffer.length === 0) { this.logger.info(`${this.name}: Write skipped`); @@ -498,6 +504,7 @@ export default abstract class Chart { update(logHour, logDay)))); } + @bindThis public async tick(major: boolean, group: string | null = null): Promise { const data = major ? await this.tickMajor(group) : await this.tickMinor(group); @@ -533,10 +540,12 @@ export default abstract class Chart { update(logHour, logDay)); } + @bindThis public resync(group: string | null = null): Promise { return this.tick(true, group); } + @bindThis public async clean(): Promise { const current = dateUTC(Chart.getCurrentDate()); @@ -572,6 +581,7 @@ export default abstract class Chart { ]); } + @bindThis public async getChartRaw(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise> { const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; @@ -676,6 +686,7 @@ export default abstract class Chart { return res; } + @bindThis public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise>> { const result = await this.getChartRaw(span, amount, cursor, group); const object = {}; diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 1660894571..7f8240b8b2 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -4,6 +4,7 @@ import type { AbuseUserReportsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AbuseUserReportEntityService { @@ -15,6 +16,7 @@ export class AbuseUserReportEntityService { ) { } + @bindThis public async pack( src: AbuseUserReport['id'] | AbuseUserReport, ) { @@ -41,6 +43,7 @@ export class AbuseUserReportEntityService { }); } + @bindThis public packMany( reports: any[], ) { diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 44110e7364..bc79ce26aa 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -4,6 +4,7 @@ import type { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepos import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { Antenna } from '@/models/entities/Antenna.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AntennaEntityService { @@ -19,6 +20,7 @@ export class AntennaEntityService { ) { } + @bindThis public async pack( src: Antenna['id'] | Antenna, ): Promise> { diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 1cc7ca11dc..781cbdcc6b 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -6,6 +6,7 @@ import type { Packed } from '@/misc/schema.js'; import type { App } from '@/models/entities/App.js'; import type { User } from '@/models/entities/User.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AppEntityService { @@ -18,6 +19,7 @@ export class AppEntityService { ) { } + @bindThis public async pack( src: App['id'] | App, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index bf8efa5f78..4a74f9c2f6 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -7,6 +7,7 @@ import type { AuthSession } from '@/models/entities/AuthSession.js'; import type { User } from '@/models/entities/User.js'; import { UserEntityService } from './UserEntityService.js'; import { AppEntityService } from './AppEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AuthSessionEntityService { @@ -18,6 +19,7 @@ export class AuthSessionEntityService { ) { } + @bindThis public async pack( src: AuthSession['id'] | AuthSession, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index 49a96037ca..c9e15207b9 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -6,6 +6,7 @@ import type { Packed } from '@/misc/schema.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class BlockingEntityService { @@ -17,6 +18,7 @@ export class BlockingEntityService { ) { } + @bindThis public async pack( src: Blocking['id'] | Blocking, me?: { id: User['id'] } | null | undefined, @@ -33,6 +35,7 @@ export class BlockingEntityService { }); } + @bindThis public packMany( blockings: any[], me: { id: User['id'] }, diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 860967443e..5e2f019a12 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { Channel } from '@/models/entities/Channel.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ChannelEntityService { @@ -29,6 +30,7 @@ export class ChannelEntityService { ) { } + @bindThis public async pack( src: Channel['id'] | Channel, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 7a5d2f7f0a..1e794391e9 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Clip } from '@/models/entities/Clip.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ClipEntityService { @@ -18,6 +19,7 @@ export class ClipEntityService { ) { } + @bindThis public async pack( src: Clip['id'] | Clip, ): Promise> { @@ -34,6 +36,7 @@ export class ClipEntityService { }); } + @bindThis public packMany( clips: Clip[], ) { diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index e0aeb70dfc..706c8c1186 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -19,6 +19,7 @@ type PackOptions = { self?: boolean, withUser?: boolean, }; +import { bindThis } from '@/decorators.js'; @Injectable() export class DriveFileEntityService { @@ -44,6 +45,7 @@ export class DriveFileEntityService { ) { } + @bindThis public validateFileName(name: string): boolean { return ( (name.trim().length > 0) && @@ -54,6 +56,7 @@ export class DriveFileEntityService { ); } + @bindThis public getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { const properties = deepClone(file.properties); @@ -67,6 +70,7 @@ export class DriveFileEntityService { return file.properties; } + @bindThis public getPublicUrl(file: DriveFile, thumbnail = false): string | null { // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) { @@ -90,6 +94,7 @@ export class DriveFileEntityService { return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); } + @bindThis public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise { const id = typeof user === 'object' ? user.id : user; @@ -103,6 +108,7 @@ export class DriveFileEntityService { return parseInt(sum, 10) ?? 0; } + @bindThis public async calcDriveUsageOfHost(host: string): Promise { const { sum } = await this.driveFilesRepository .createQueryBuilder('file') @@ -114,6 +120,7 @@ export class DriveFileEntityService { return parseInt(sum, 10) ?? 0; } + @bindThis public async calcDriveUsageOfLocal(): Promise { const { sum } = await this.driveFilesRepository .createQueryBuilder('file') @@ -125,6 +132,7 @@ export class DriveFileEntityService { return parseInt(sum, 10) ?? 0; } + @bindThis public async calcDriveUsageOfRemote(): Promise { const { sum } = await this.driveFilesRepository .createQueryBuilder('file') @@ -136,6 +144,7 @@ export class DriveFileEntityService { return parseInt(sum, 10) ?? 0; } + @bindThis public async pack( src: DriveFile['id'] | DriveFile, options?: PackOptions, @@ -169,6 +178,7 @@ export class DriveFileEntityService { }); } + @bindThis public async packNullable( src: DriveFile['id'] | DriveFile, options?: PackOptions, @@ -203,6 +213,7 @@ export class DriveFileEntityService { }); } + @bindThis public async packMany( files: (DriveFile['id'] | DriveFile)[], options?: PackOptions, diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 5761fa37bc..0bb0f1754e 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DriveFolderEntityService { @@ -19,6 +20,7 @@ export class DriveFolderEntityService { ) { } + @bindThis public async pack( src: DriveFolder['id'] | DriveFolder, options?: { diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index fc09b5a2c7..08d83a2753 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class EmojiEntityService { @@ -18,6 +19,7 @@ export class EmojiEntityService { ) { } + @bindThis public async pack( src: Emoji['id'] | Emoji, ): Promise> { @@ -34,6 +36,7 @@ export class EmojiEntityService { }; } + @bindThis public packMany( emojis: any[], ) { diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index 4a60c1263f..88c91d0f21 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { FollowRequest } from '@/models/entities/FollowRequest.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class FollowRequestEntityService { @@ -18,6 +19,7 @@ export class FollowRequestEntityService { ) { } + @bindThis public async pack( src: FollowRequest['id'] | FollowRequest, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index c7e040a57b..a833ae719b 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -31,6 +31,7 @@ type RemoteFolloweeFollowing = Following & { followeeInbox: string; followeeSharedInbox: string; }; +import { bindThis } from '@/decorators.js'; @Injectable() export class FollowingEntityService { @@ -42,22 +43,27 @@ export class FollowingEntityService { ) { } + @bindThis public isLocalFollower(following: Following): following is LocalFollowerFollowing { return following.followerHost == null; } + @bindThis public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { return following.followerHost != null; } + @bindThis public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { return following.followeeHost == null; } + @bindThis public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { return following.followeeHost != null; } + @bindThis public async pack( src: Following['id'] | Following, me?: { id: User['id'] } | null | undefined, @@ -84,6 +90,7 @@ export class FollowingEntityService { }); } + @bindThis public packMany( followings: any[], me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index 7e599113cc..8b15ffc2bb 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { GalleryLike } from '@/models/entities/GalleryLike.js'; import { UserEntityService } from './UserEntityService.js'; import { GalleryPostEntityService } from './GalleryPostEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GalleryLikeEntityService { @@ -19,6 +20,7 @@ export class GalleryLikeEntityService { ) { } + @bindThis public async pack( src: GalleryLike['id'] | GalleryLike, me?: any, @@ -31,6 +33,7 @@ export class GalleryLikeEntityService { }; } + @bindThis public packMany( likes: any[], me: any, diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index ca98687d7b..ab29e7dba1 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { GalleryPost } from '@/models/entities/GalleryPost.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GalleryPostEntityService { @@ -23,6 +24,7 @@ export class GalleryPostEntityService { ) { } + @bindThis public async pack( src: GalleryPost['id'] | GalleryPost, me?: { id: User['id'] } | null | undefined, @@ -47,6 +49,7 @@ export class GalleryPostEntityService { }); } + @bindThis public packMany( posts: GalleryPost[], me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index 511992c44f..f79b821222 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Hashtag } from '@/models/entities/Hashtag.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class HashtagEntityService { @@ -18,6 +19,7 @@ export class HashtagEntityService { ) { } + @bindThis public async pack( src: Hashtag, ): Promise> { @@ -32,6 +34,7 @@ export class HashtagEntityService { }; } + @bindThis public packMany( hashtags: Hashtag[], ) { diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index f4fe9a17d3..5a7ceb89a3 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { Instance } from '@/models/entities/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class InstanceEntityService { @@ -19,6 +20,7 @@ export class InstanceEntityService { ) { } + @bindThis public async pack( instance: Instance, ): Promise> { @@ -50,6 +52,7 @@ export class InstanceEntityService { }; } + @bindThis public packMany( instances: Instance[], ) { diff --git a/packages/backend/src/core/entities/MessagingMessageEntityService.ts b/packages/backend/src/core/entities/MessagingMessageEntityService.ts index b7c42a5760..cdb752dd81 100644 --- a/packages/backend/src/core/entities/MessagingMessageEntityService.ts +++ b/packages/backend/src/core/entities/MessagingMessageEntityService.ts @@ -9,6 +9,7 @@ import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; import { UserGroupEntityService } from './UserGroupEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class MessagingMessageEntityService { @@ -22,6 +23,7 @@ export class MessagingMessageEntityService { ) { } + @bindThis public async pack( src: MessagingMessage['id'] | MessagingMessage, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index 2f508710b8..ab61797910 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { ModerationLog } from '@/models/entities/ModerationLog.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ModerationLogEntityService { @@ -18,6 +19,7 @@ export class ModerationLogEntityService { ) { } + @bindThis public async pack( src: ModerationLog['id'] | ModerationLog, ) { @@ -35,6 +37,7 @@ export class ModerationLogEntityService { }); } + @bindThis public packMany( reports: any[], ) { diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index 862be009da..4f02ef4087 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Muting } from '@/models/entities/Muting.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class MutingEntityService { @@ -18,6 +19,7 @@ export class MutingEntityService { ) { } + @bindThis public async pack( src: Muting['id'] | Muting, me?: { id: User['id'] } | null | undefined, @@ -35,6 +37,7 @@ export class MutingEntityService { }); } + @bindThis public packMany( mutings: any[], me: { id: User['id'] }, diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 5605cf8ce6..73d3184957 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,6 +16,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteEntityService implements OnModuleInit { @@ -68,6 +69,7 @@ export class NoteEntityService implements OnModuleInit { this.reactionService = this.moduleRef.get('ReactionService'); } + @bindThis private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) let hide = false; @@ -128,6 +130,7 @@ export class NoteEntityService implements OnModuleInit { } } + @bindThis private async populatePoll(note: Note, meId: User['id'] | null) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); const choices = poll.choices.map(c => ({ @@ -166,6 +169,7 @@ export class NoteEntityService implements OnModuleInit { }; } + @bindThis private async populateMyReaction(note: Note, meId: User['id'], _hint_?: { myReactions: Map; }) { @@ -191,6 +195,7 @@ export class NoteEntityService implements OnModuleInit { return undefined; } + @bindThis public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { // This code must always be synchronized with the checks in generateVisibilityQuery. // visibility が specified かつ自分が指定されていなかったら非表示 @@ -244,6 +249,7 @@ export class NoteEntityService implements OnModuleInit { return true; } + @bindThis public async pack( src: Note['id'] | Note, me?: { id: User['id'] } | null | undefined, @@ -353,6 +359,7 @@ export class NoteEntityService implements OnModuleInit { return packed; } + @bindThis public async packMany( notes: Note[], me?: { id: User['id'] } | null | undefined, @@ -388,6 +395,7 @@ export class NoteEntityService implements OnModuleInit { }))); } + @bindThis public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { // 指定したユーザーの指定したノートのリノートがいくつあるか数える const query = this.notesRepository.createQueryBuilder('note') diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index 1a68a5c628..aa5c354b6d 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import { UserEntityService } from './UserEntityService.js'; import { NoteEntityService } from './NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteFavoriteEntityService { @@ -19,6 +20,7 @@ export class NoteFavoriteEntityService { ) { } + @bindThis public async pack( src: NoteFavorite['id'] | NoteFavorite, me?: { id: User['id'] } | null | undefined, @@ -33,6 +35,7 @@ export class NoteFavoriteEntityService { }; } + @bindThis public packMany( favorites: any[], me: { id: User['id'] }, diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 47008ee08e..eba6f9d908 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -11,6 +11,7 @@ import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import { ModuleRef } from '@nestjs/core'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteReactionEntityService implements OnModuleInit { @@ -36,6 +37,7 @@ export class NoteReactionEntityService implements OnModuleInit { this.reactionService = this.moduleRef.get('ReactionService'); } + @bindThis public async pack( src: NoteReaction['id'] | NoteReaction, me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index c415599fea..346faae6b0 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -13,6 +13,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NotificationEntityService implements OnModuleInit { @@ -47,6 +48,7 @@ export class NotificationEntityService implements OnModuleInit { this.customEmojiService = this.moduleRef.get('CustomEmojiService'); } + @bindThis public async pack( src: Notification['id'] | Notification, options: { @@ -120,6 +122,7 @@ export class NotificationEntityService implements OnModuleInit { }); } + @bindThis public async packMany( notifications: Notification[], meId: User['id'], diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index 004443759b..48e45dd019 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -9,6 +9,7 @@ import type { Page } from '@/models/entities/Page.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class PageEntityService { @@ -27,6 +28,7 @@ export class PageEntityService { ) { } + @bindThis public async pack( src: Page['id'] | Page, me?: { id: User['id'] } | null | undefined, @@ -99,6 +101,7 @@ export class PageEntityService { }); } + @bindThis public packMany( pages: Page[], me?: { id: User['id'] } | null | undefined, diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index 62d9c82ca6..d3e45783dd 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { PageLike } from '@/models/entities/PageLike.js'; import { UserEntityService } from './UserEntityService.js'; import { PageEntityService } from './PageEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class PageLikeEntityService { @@ -19,6 +20,7 @@ export class PageLikeEntityService { ) { } + @bindThis public async pack( src: PageLike['id'] | PageLike, me?: { id: User['id'] } | null | undefined, @@ -31,6 +33,7 @@ export class PageLikeEntityService { }; } + @bindThis public packMany( likes: any[], me: { id: User['id'] }, diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts index fd89662f7d..c402644742 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Signin } from '@/models/entities/Signin.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SigninEntityService { @@ -18,6 +19,7 @@ export class SigninEntityService { ) { } + @bindThis public async pack( src: Signin, ) { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index c691eaebdf..4a027d1de6 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -41,6 +41,7 @@ function isRemoteUser(user: T): user is T & { function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); } +import { bindThis } from '@/decorators.js'; @Injectable() export class UserEntityService implements OnModuleInit { @@ -143,6 +144,7 @@ export class UserEntityService implements OnModuleInit { public isLocalUser = isLocalUser; public isRemoteUser = isRemoteUser; + @bindThis public async getRelation(me: User['id'], target: User['id']) { return awaitAll({ id: target, @@ -198,6 +200,7 @@ export class UserEntityService implements OnModuleInit { }); } + @bindThis public async getHasUnreadMessagingMessage(userId: User['id']): Promise { const mute = await this.mutingsRepository.findBy({ muterId: userId, @@ -227,6 +230,7 @@ export class UserEntityService implements OnModuleInit { return withUser || withGroups.some(x => x); } + @bindThis public async getHasUnreadAnnouncement(userId: User['id']): Promise { const reads = await this.announcementReadsRepository.findBy({ userId: userId, @@ -239,6 +243,7 @@ export class UserEntityService implements OnModuleInit { return count > 0; } + @bindThis public async getHasUnreadAntenna(userId: User['id']): Promise { const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); @@ -250,6 +255,7 @@ export class UserEntityService implements OnModuleInit { return unread != null; } + @bindThis public async getHasUnreadChannel(userId: User['id']): Promise { const channels = await this.channelFollowingsRepository.findBy({ followerId: userId }); @@ -261,6 +267,7 @@ export class UserEntityService implements OnModuleInit { return unread != null; } + @bindThis public async getHasUnreadNotification(userId: User['id']): Promise { const mute = await this.mutingsRepository.findBy({ muterId: userId, @@ -279,6 +286,7 @@ export class UserEntityService implements OnModuleInit { return count > 0; } + @bindThis public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { const count = await this.followRequestsRepository.countBy({ followeeId: userId, @@ -287,6 +295,7 @@ export class UserEntityService implements OnModuleInit { return count > 0; } + @bindThis public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { if (user.hideOnlineStatus) return 'unknown'; if (user.lastActiveDate == null) return 'unknown'; @@ -298,6 +307,7 @@ export class UserEntityService implements OnModuleInit { ); } + @bindThis public async getAvatarUrl(user: User): Promise { if (user.avatar) { return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); @@ -309,6 +319,7 @@ export class UserEntityService implements OnModuleInit { } } + @bindThis public getAvatarUrlSync(user: User): string { if (user.avatar) { return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); @@ -317,6 +328,7 @@ export class UserEntityService implements OnModuleInit { } } + @bindThis public getIdenticonUrl(userId: User['id']): string { return `${this.config.url}/identicon/${userId}`; } diff --git a/packages/backend/src/core/entities/UserGroupEntityService.ts b/packages/backend/src/core/entities/UserGroupEntityService.ts index e399197618..0674a76723 100644 --- a/packages/backend/src/core/entities/UserGroupEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserGroupEntityService { @@ -21,6 +22,7 @@ export class UserGroupEntityService { ) { } + @bindThis public async pack( src: UserGroup['id'] | UserGroup, ): Promise> { diff --git a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts index f5c9be3475..0fba1426f4 100644 --- a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts @@ -8,6 +8,7 @@ import type { User } from '@/models/entities/User.js'; import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; import { UserEntityService } from './UserEntityService.js'; import { UserGroupEntityService } from './UserGroupEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserGroupInvitationEntityService { @@ -19,6 +20,7 @@ export class UserGroupInvitationEntityService { ) { } + @bindThis public async pack( src: UserGroupInvitation['id'] | UserGroupInvitation, ) { @@ -30,6 +32,7 @@ export class UserGroupInvitationEntityService { }; } + @bindThis public packMany( invitations: any[], ) { diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index e2b0814914..f2e0426928 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -7,6 +7,7 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { UserList } from '@/models/entities/UserList.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UserListEntityService { @@ -21,6 +22,7 @@ export class UserListEntityService { ) { } + @bindThis public async pack( src: UserList['id'] | UserList, ): Promise> { diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts index dbad576abe..8cdfb703f1 100644 --- a/packages/backend/src/daemons/JanitorService.ts +++ b/packages/backend/src/daemons/JanitorService.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { AttestationChallengesRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const interval = 30 * 60 * 1000; @@ -19,6 +20,7 @@ export class JanitorService implements OnApplicationShutdown { /** * Clean up database occasionally */ + @bindThis public start(): void { const tick = async () => { await this.attestationChallengesRepository.delete({ @@ -31,6 +33,7 @@ export class JanitorService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } + @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.intervalId); } diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index 931de19067..7b47d78a17 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Xev from 'xev'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; +import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -20,6 +21,7 @@ export class QueueStatsService implements OnApplicationShutdown { /** * Report queue stats regularly */ + @bindThis public start(): void { const log = [] as any[]; @@ -71,6 +73,7 @@ export class QueueStatsService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } + @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.intervalId); } diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index e40912442d..7971f9e810 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -3,6 +3,7 @@ import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -23,6 +24,7 @@ export class ServerStatsService implements OnApplicationShutdown { /** * Report server stats regularly */ + @bindThis public start(): void { const log = [] as any[]; @@ -61,6 +63,7 @@ export class ServerStatsService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } + @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.intervalId); } diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts new file mode 100644 index 0000000000..94b1c4be8c --- /dev/null +++ b/packages/backend/src/decorators.ts @@ -0,0 +1,41 @@ +// https://github.com/andreypopp/autobind-decorator + +/** + * Return a descriptor removing the value and returning a getter + * The getter will return a .bind version of the function + * and memoize the result against a symbol on the instance + */ +export function bindThis(target, key, descriptor) { + let fn = descriptor.value; + + if (typeof fn !== 'function') { + throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`); + } + + return { + configurable: true, + get() { + // eslint-disable-next-line no-prototype-builtins + if (this === target.prototype || this.hasOwnProperty(key) || + typeof fn !== 'function') { + return fn; + } + + const boundFn = fn.bind(this); + Object.defineProperty(this, key, { + configurable: true, + get() { + return boundFn; + }, + set(value) { + fn = value; + delete this[key]; + }, + }); + return boundFn; + }, + set(value) { + fn = value; + }, + }; +} diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 6722220681..d09b479c42 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -2,6 +2,7 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import { default as convertColor } from 'color-convert'; import { format as dateFormat } from 'date-fns'; +import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; type Domain = { @@ -26,12 +27,14 @@ export default class Logger { this.syslogClient = syslogClient; } + @bindThis public createSubLogger(domain: string, color?: string, store = true): Logger { const logger = new Logger(domain, color, store); logger.parentLogger = this; return logger; } + @bindThis private log(level: Level, message: string, data?: Record | null, important = false, subDomains: Domain[] = [], store = true): void { if (envOption.quiet) return; if (!this.store) store = false; @@ -80,6 +83,7 @@ export default class Logger { } } + @bindThis public error(x: string | Error, data?: Record | null, important = false): void { // 実行を継続できない状況で使う if (x instanceof Error) { data = data ?? {}; @@ -92,20 +96,24 @@ export default class Logger { } } + @bindThis public warn(message: string, data?: Record | null, important = false): void { // 実行を継続できるが改善すべき状況で使う this.log('warning', message, data, important); } + @bindThis public succ(message: string, data?: Record | null, important = false): void { // 何かに成功した状況で使う this.log('success', message, data, important); } + @bindThis public debug(message: string, data?: Record | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) if (process.env.NODE_ENV !== 'production' || envOption.verbose) { this.log('debug', message, data, important); } } + @bindThis public info(message: string, data?: Record | null, important = false): void { // それ以外 this.log('info', message, data, important); } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index e5b911ed32..69512498f8 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,3 +1,5 @@ +import { bindThis } from '@/decorators.js'; + export class Cache { public cache: Map; private lifetime: number; @@ -7,6 +9,7 @@ export class Cache { this.lifetime = lifetime; } + @bindThis public set(key: string | null, value: T): void { this.cache.set(key, { date: Date.now(), @@ -14,6 +17,7 @@ export class Cache { }); } + @bindThis public get(key: string | null): T | undefined { const cached = this.cache.get(key); if (cached == null) return undefined; @@ -24,6 +28,7 @@ export class Cache { return cached.value; } + @bindThis public delete(key: string | null) { this.cache.delete(key); } @@ -32,6 +37,7 @@ export class Cache { * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ + @bindThis public async fetch(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { @@ -56,6 +62,7 @@ export class Cache { * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ + @bindThis public async fetchMaybe(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts index 4fa398763a..e304a8adac 100644 --- a/packages/backend/src/misc/i18n.ts +++ b/packages/backend/src/misc/i18n.ts @@ -5,12 +5,13 @@ export class I18n> { this.locale = locale; //#region BIND - this.t = this.t.bind(this); + //this.t = this.t.bind(this); //#endregion } // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも + @bindThis public t(key: string, args?: Record): string { try { let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts index 2beb31e24e..0f9f7a6a96 100644 --- a/packages/backend/src/postgre.ts +++ b/packages/backend/src/postgre.ts @@ -72,6 +72,7 @@ import { Channel } from '@/models/entities/Channel.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; export const dbLogger = new MisskeyLogger('db'); @@ -79,32 +80,39 @@ export const dbLogger = new MisskeyLogger('db'); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); class MyCustomLogger implements Logger { + @bindThis private highlight(sql: string) { return highlight.highlight(sql, { language: 'sql', ignoreIllegals: true, }); } + @bindThis public logQuery(query: string, parameters?: any[]) { sqlLogger.info(this.highlight(query).substring(0, 100)); } + @bindThis public logQueryError(error: string, query: string, parameters?: any[]) { sqlLogger.error(this.highlight(query)); } + @bindThis public logQuerySlow(time: number, query: string, parameters?: any[]) { sqlLogger.warn(this.highlight(query)); } + @bindThis public logSchemaBuild(message: string) { sqlLogger.info(message); } + @bindThis public log(message: string) { sqlLogger.info(message); } + @bindThis public logMigration(message: string) { sqlLogger.info(message); } diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts index 58384c4d1b..e5568ab9bf 100644 --- a/packages/backend/src/queue/DbQueueProcessorsService.ts +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -16,6 +16,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DbQueueProcessorsService { @@ -39,6 +40,7 @@ export class DbQueueProcessorsService { ) { } + @bindThis public start(q: Bull.Queue): void { q.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done)); q.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done)); diff --git a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts index 3ff3dd090c..c95e1c1ba6 100644 --- a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts +++ b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts @@ -5,6 +5,7 @@ import type { Config } from '@/config.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ObjectStorageQueueProcessorsService { @@ -17,6 +18,7 @@ export class ObjectStorageQueueProcessorsService { ) { } + @bindThis public start(q: Bull.Queue): void { q.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job)); q.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done)); diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts index a311470cc9..3a8a734f10 100644 --- a/packages/backend/src/queue/QueueLoggerService.ts +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class QueueLoggerService { diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 8c300d479c..1d2feb5ef8 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class QueueProcessorService { @@ -35,6 +36,7 @@ export class QueueProcessorService { this.logger = this.queueLoggerService.logger; } + @bindThis public start() { function renderError(e: Error): any { if (e) { // 何故かeがundefinedで来ることがある diff --git a/packages/backend/src/queue/SystemQueueProcessorsService.ts b/packages/backend/src/queue/SystemQueueProcessorsService.ts index a8af92b9ba..1ce4152b2c 100644 --- a/packages/backend/src/queue/SystemQueueProcessorsService.ts +++ b/packages/backend/src/queue/SystemQueueProcessorsService.ts @@ -7,6 +7,7 @@ import { CleanChartsProcessorService } from './processors/CleanChartsProcessorSe import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SystemQueueProcessorsService { @@ -22,6 +23,7 @@ export class SystemQueueProcessorsService { ) { } + @bindThis public start(q: Bull.Queue): void { q.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done)); q.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done)); diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index e91cba9d10..7a1e3e71be 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -7,6 +7,7 @@ import type Logger from '@/logger.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CheckExpiredMutingsProcessorService { @@ -25,6 +26,7 @@ export class CheckExpiredMutingsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Checking expired mutings...'); diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index e8e90f1422..c57086240a 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -17,6 +17,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CleanChartsProcessorService { @@ -44,6 +45,7 @@ export class CleanChartsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('clean-charts'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Clean charts...'); diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 6eb457ce9f..8ca39a9677 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -6,6 +6,7 @@ import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CleanProcessorService { @@ -23,6 +24,7 @@ export class CleanProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('clean'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Cleaning...'); diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index a4fd8c502d..5a33c27188 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -7,6 +7,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class CleanRemoteFilesProcessorService { @@ -25,6 +26,7 @@ export class CleanRemoteFilesProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-files'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Deleting cached remote files...'); diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 5e4c8bdd69..e36a78de6a 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -11,6 +11,7 @@ import { EmailService } from '@/core/EmailService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserDeleteJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteAccountProcessorService { @@ -39,6 +40,7 @@ export class DeleteAccountProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('delete-account'); } + @bindThis public async process(job: Bull.Job): Promise { this.logger.info(`Deleting account of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 682382b2db..fa0c1733f6 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -8,6 +8,7 @@ import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteDriveFilesProcessorService { @@ -29,6 +30,7 @@ export class DeleteDriveFilesProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('delete-drive-files'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index 6740643fe2..2fb2f56f8d 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -6,6 +6,7 @@ import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { ObjectStorageFileJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteFileProcessorService { @@ -21,6 +22,7 @@ export class DeleteFileProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('delete-file'); } + @bindThis public async process(job: Bull.Job): Promise { const key: string = job.data.key; diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 2a4b201a7d..58969d550e 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -18,6 +18,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DeliverJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DeliverProcessorService { @@ -50,6 +51,7 @@ export class DeliverProcessorService { this.latest = null; } + @bindThis public async process(job: Bull.Job): Promise { const { host } = new URL(job.data.to); diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 2fc7fe219e..21d2dc9efc 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -8,6 +8,7 @@ import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { EndedPollNotificationJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class EndedPollNotificationProcessorService { @@ -29,6 +30,7 @@ export class EndedPollNotificationProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { const note = await this.notesRepository.findOneBy({ id: job.data.noteId }); if (note == null || !note.hasPoll) { diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index db149b68cf..5b3c1a415b 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportBlockingProcessorService { @@ -34,6 +35,7 @@ export class ExportBlockingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-blocking'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index f8f261b478..87b23f1891 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -14,6 +14,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportCustomEmojisProcessorService { @@ -36,6 +37,7 @@ export class ExportCustomEmojisProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info('Exporting custom emojis ...'); diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 1e3fba06b5..064b126e44 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -13,6 +13,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportFollowingProcessorService { @@ -38,6 +39,7 @@ export class ExportFollowingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-following'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting following of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index e263c245fd..94c7ea8a46 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportMutingProcessorService { @@ -37,6 +38,7 @@ export class ExportMutingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-muting'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting muting of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 533d4bd7c6..8431829e91 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -13,6 +13,7 @@ import type { Note } from '@/models/entities/Note.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportNotesProcessorService { @@ -37,6 +38,7 @@ export class ExportNotesProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting notes of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 8c3e3dbe17..a8daa5e5ee 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ExportUserListsProcessorService { @@ -37,6 +38,7 @@ export class ExportUserListsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 9442a60d8d..2eed420e96 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ImportBlockingProcessorService { @@ -39,6 +40,7 @@ export class ImportBlockingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('import-blocking'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Importing blocking of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 492f17f9ff..0061c2a8f7 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -10,6 +10,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { createTempDir } from '@/misc/create-temp.js'; import { DriveService } from '@/core/DriveService.js'; import { DownloadService } from '@/core/DownloadService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; @@ -43,6 +44,7 @@ export class ImportCustomEmojisProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('import-custom-emojis'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info('Importing custom emojis ...'); diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 667f7279fb..b61846d747 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ImportFollowingProcessorService { @@ -36,6 +37,7 @@ export class ImportFollowingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('import-following'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Importing following of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index f3c16e73d5..21236da2ef 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -12,6 +12,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ImportMutingProcessorService { @@ -36,6 +37,7 @@ export class ImportMutingProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('import-muting'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Importing muting of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 1519877c5f..1bec77b837 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -13,6 +13,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ImportUserListsProcessorService { @@ -44,6 +45,7 @@ export class ImportUserListsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('import-user-lists'); } + @bindThis public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Importing user lists of ${job.data.user.id} ...`); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 8f1c474020..c032122caf 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -24,6 +24,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DeliverJobData, InboxJobData } from '../types.js'; @@ -60,6 +61,7 @@ export class InboxProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); } + @bindThis public async process(job: Bull.Job): Promise { const signature = job.data.signature; // HTTP-signature const activity = job.data.activity; diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index bf2fdeb7a0..1a8fe65a4f 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -17,6 +17,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ResyncChartsProcessorService { @@ -44,6 +45,7 @@ export class ResyncChartsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('resync-charts'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Resync charts...'); diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 96607e1d68..323e5227cd 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -17,6 +17,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; +import { bindThis } from '@/decorators.js'; @Injectable() export class TickChartsProcessorService { @@ -44,6 +45,7 @@ export class TickChartsProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('tick-charts'); } + @bindThis public async process(job: Bull.Job>, done: () => void): Promise { this.logger.info('Tick charts...'); diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 43e3f37201..183ef07477 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -9,6 +9,7 @@ import { StatusError } from '@/misc/status-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { WebhookDeliverJobData } from '../types.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class WebhookDeliverProcessorService { @@ -27,6 +28,7 @@ export class WebhookDeliverProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('webhook'); } + @bindThis public async process(job: Bull.Job): Promise { try { this.logger.debug(`delivering ${job.data.webhookId}`); diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 015c8f2b4c..94a277f4a4 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -18,6 +18,7 @@ import type { Note } from '@/models/entities/Note.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import type { FindOptionsWhere } from 'typeorm'; const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; @@ -57,9 +58,10 @@ export class ActivityPubServerService { private userKeypairStoreService: UserKeypairStoreService, private queryService: QueryService, ) { - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis private setResponseType(request: FastifyRequest, reply: FastifyReply): void { const accept = request.accepts().type([ACTIVITY_JSON, LD_JSON]); if (accept === LD_JSON) { @@ -73,6 +75,7 @@ export class ActivityPubServerService { * Pack Create or Announce Activity * @param note Note */ + @bindThis private async packActivity(note: Note): Promise { if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); @@ -82,6 +85,7 @@ export class ActivityPubServerService { return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); } + @bindThis private inbox(request: FastifyRequest, reply: FastifyReply) { let signature; @@ -97,6 +101,7 @@ export class ActivityPubServerService { reply.code(202); } + @bindThis private async followers( request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, @@ -184,6 +189,7 @@ export class ActivityPubServerService { } } + @bindThis private async following( request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, @@ -271,6 +277,7 @@ export class ActivityPubServerService { } } + @bindThis private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { const userId = request.params.user; @@ -304,6 +311,7 @@ export class ActivityPubServerService { return (this.apRendererService.renderActivity(rendered)); } + @bindThis private async outbox( request: FastifyRequest<{ Params: { user: string; }; @@ -390,6 +398,7 @@ export class ActivityPubServerService { } } + @bindThis private async userInfo(request: FastifyRequest, reply: FastifyReply, user: User | null) { if (user == null) { reply.code(404); @@ -401,6 +410,7 @@ export class ActivityPubServerService { return (this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser))); } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addConstraintStrategy({ name: 'apOrHtml', diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 088e780d69..b7ab549611 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -19,6 +19,7 @@ import { InternalStorageService } from '@/core/InternalStorageService.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -45,9 +46,10 @@ export class FileServerService { ) { this.logger = this.loggerService.getLogger('server', 'gray', false); - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis public commonReadableHandlerGenerator(reply: FastifyReply) { return (err: Error): void => { this.logger.error(err); @@ -56,6 +58,7 @@ export class FileServerService { }; } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addHook('onRequest', (request, reply, done) => { reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); @@ -80,6 +83,7 @@ export class FileServerService { done(); } + @bindThis private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) { const key = request.params.key; diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts index 4d7bbdf599..733a7feeb5 100644 --- a/packages/backend/src/server/MediaProxyServerService.ts +++ b/packages/backend/src/server/MediaProxyServerService.ts @@ -14,6 +14,7 @@ import { StatusError } from '@/misc/status-error.js'; import type Logger from '@/logger.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class MediaProxyServerService { @@ -30,9 +31,10 @@ export class MediaProxyServerService { ) { this.logger = this.loggerService.getLogger('server', 'gray', false); - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addHook('onRequest', (request, reply, done) => { reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); @@ -47,6 +49,7 @@ export class MediaProxyServerService { done(); } + @bindThis private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) { const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index b85925f53e..0f3cc36dae 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; const nodeinfo2_1path = '/nodeinfo/2.1'; const nodeinfo2_0path = '/nodeinfo/2.0'; @@ -27,9 +28,10 @@ export class NodeinfoServerService { private userEntityService: UserEntityService, private metaService: MetaService, ) { - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis public getLinks() { return [/* (awaiting release) { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', @@ -40,6 +42,7 @@ export class NodeinfoServerService { }]; } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { const nodeinfo2 = async () => { const now = Date.now(); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 96159cfc53..075b9cdff7 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -23,6 +23,7 @@ import { WellKnownServerService } from './WellKnownServerService.js'; import { MediaProxyServerService } from './MediaProxyServerService.js'; import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ServerService { @@ -53,6 +54,7 @@ export class ServerService { this.logger = this.loggerService.getLogger('server', 'gray', false); } + @bindThis public launch() { const fastify = Fastify({ trustProxy: true, diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 412c608313..ea34ad5b0f 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -10,6 +10,7 @@ import type { User } from '@/models/entities/User.js'; import * as Acct from '@/misc/acct.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import type { FindOptionsWhere } from 'typeorm'; +import { bindThis } from '@/decorators.js'; @Injectable() export class WellKnownServerService { @@ -22,9 +23,10 @@ export class WellKnownServerService { private nodeinfoServerService: NodeinfoServerService, ) { - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { const XRD = (...x: { element: string, value?: string, attributes?: Record }[]) => `${x.map(({ element, value, attributes }) => diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 2e72cdf9f8..fb1e17790c 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -12,6 +12,7 @@ import type Logger from '@/logger.js'; import type { UserIpsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; +import { bindThis } from '@/decorators.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -50,6 +51,7 @@ export class ApiCallService implements OnApplicationShutdown { }, 1000 * 60 * 60); } + @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record, Querystring: Record }>, @@ -90,6 +92,7 @@ export class ApiCallService implements OnApplicationShutdown { }); } + @bindThis public async handleMultipartRequest( endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record, Querystring: Record }>, @@ -140,6 +143,7 @@ export class ApiCallService implements OnApplicationShutdown { }); } + @bindThis private send(reply: FastifyReply, x?: any, y?: ApiError) { if (x == null) { reply.code(204); @@ -160,6 +164,7 @@ export class ApiCallService implements OnApplicationShutdown { } } + @bindThis private async logIp(request: FastifyRequest, user: ILocalUser) { const meta = await this.metaService.fetch(); if (!meta.enableIpLogging) return; @@ -183,6 +188,7 @@ export class ApiCallService implements OnApplicationShutdown { } } + @bindThis private async call( ep: IEndpoint & { exec: any }, user: CacheableLocalUser | null | undefined, @@ -315,6 +321,7 @@ export class ApiCallService implements OnApplicationShutdown { }); } + @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.userIpHistoriesClearIntervalId); } diff --git a/packages/backend/src/server/api/ApiLoggerService.ts b/packages/backend/src/server/api/ApiLoggerService.ts index c4fb25036e..cabd65fd3e 100644 --- a/packages/backend/src/server/api/ApiLoggerService.ts +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApiLoggerService { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index cf3f2deebf..b17456d0e2 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -14,6 +14,7 @@ import { SigninApiService } from './SigninApiService.js'; import { GithubServerService } from './integration/GithubServerService.js'; import { DiscordServerService } from './integration/DiscordServerService.js'; import { TwitterServerService } from './integration/TwitterServerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApiServerService { @@ -40,9 +41,10 @@ export class ApiServerService { private discordServerService: DiscordServerService, private twitterServerService: TwitterServerService, ) { - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.register(cors, { origin: '*', diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index ad387c4732..8b39f6c924 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -7,6 +7,7 @@ import { Cache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; +import { bindThis } from '@/decorators.js'; export class AuthenticationError extends Error { constructor(message: string) { @@ -34,6 +35,7 @@ export class AuthenticateService { this.appCache = new Cache(Infinity); } + @bindThis public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> { if (token == null) { return [null, null]; diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 70ab46ec35..c7f9916f97 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -5,6 +5,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GetterService { @@ -22,6 +23,7 @@ export class GetterService { /** * Get note for API processing */ + @bindThis public async getNote(noteId: Note['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); @@ -35,6 +37,7 @@ export class GetterService { /** * Get user for API processing */ + @bindThis public async getUser(userId: User['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); @@ -48,6 +51,7 @@ export class GetterService { /** * Get remote user for API processing */ + @bindThis public async getRemoteUser(userId: User['id']) { const user = await this.getUser(userId); @@ -61,6 +65,7 @@ export class GetterService { /** * Get local user for API processing */ + @bindThis public async getLocalUser(userId: User['id']) { const user = await this.getUser(userId); diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 35f28bfd63..94a15f94bb 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -5,6 +5,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { IEndpointMeta } from './endpoints.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class RateLimiterService { @@ -19,6 +20,7 @@ export class RateLimiterService { this.logger = this.loggerService.getLogger('limiter'); } + @bindThis public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) { return new Promise((ok, reject) => { if (process.env.NODE_ENV === 'test') ok(); diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 8b3d86e5a6..b633c2888f 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -13,6 +13,7 @@ import { IdService } from '@/core/IdService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SigninApiService { @@ -42,6 +43,7 @@ export class SigninApiService { ) { } + @bindThis public async signin( request: FastifyRequest<{ Body: { diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 18a1d6c088..96a89956f9 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -7,6 +7,7 @@ import { IdService } from '@/core/IdService.js'; import type { ILocalUser } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SigninService { @@ -23,6 +24,7 @@ export class SigninService { ) { } + @bindThis public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) { setImmediate(async () => { // Append signin history diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 771858d091..59676426af 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -14,6 +14,7 @@ import { EmailService } from '@/core/EmailService.js'; import { ILocalUser } from '@/models/entities/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { SigninService } from './SigninService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class SignupApiService { @@ -43,6 +44,7 @@ export class SignupApiService { ) { } + @bindThis public async signup( request: FastifyRequest<{ Body: { @@ -165,6 +167,7 @@ export class SignupApiService { } } + @bindThis public async signupPending(request: FastifyRequest<{ Body: { code: string; } }>, reply: FastifyReply) { const body = request.body; diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 46eaf8566e..487eef2d50 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -13,6 +13,7 @@ import MainStreamConnection from './stream/index.js'; import { ChannelsService } from './stream/ChannelsService.js'; import type { ParsedUrlQuery } from 'querystring'; import type * as http from 'node:http'; +import { bindThis } from '@/decorators.js'; @Injectable() export class StreamingApiServerService { @@ -49,6 +50,7 @@ export class StreamingApiServerService { ) { } + @bindThis public attachStreamingApi(server: http.Server) { // Init websocket server const ws = new websocket.server({ diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 53de8d9495..9fc1391570 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -8,6 +8,7 @@ import { UserSuspendService } from '@/core/UserSuspendService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; export const meta = { tags: ['admin'], @@ -79,6 +80,7 @@ export default class extends Endpoint { }); } + @bindThis private async unFollowAll(follower: User) { const followings = await this.followingsRepository.findBy({ followerId: follower.id, @@ -97,6 +99,7 @@ export default class extends Endpoint { } } + @bindThis private async readAllNotify(notifier: User) { await this.notificationsRepository.update({ notifierId: notifier.id, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index c218ec4642..1068a2eec7 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -112,6 +113,7 @@ export default class extends Endpoint { /*** * URIからUserかNoteを解決する */ + @bindThis private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); @@ -144,6 +146,7 @@ export default class extends Endpoint { ); } + @bindThis private async mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { diff --git a/packages/backend/src/server/api/integration/DiscordServerService.ts b/packages/backend/src/server/api/integration/DiscordServerService.ts index 93c22a6c0b..a7f39a78d0 100644 --- a/packages/backend/src/server/api/integration/DiscordServerService.ts +++ b/packages/backend/src/server/api/integration/DiscordServerService.ts @@ -14,6 +14,7 @@ import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { SigninService } from '../SigninService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class DiscordServerService { @@ -36,9 +37,10 @@ export class DiscordServerService { private metaService: MetaService, private signinService: SigninService, ) { - this.create = this.create.bind(this); + //this.create = this.create.bind(this); } + @bindThis public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/disconnect/discord', async (request, reply) => { if (!this.compareOrigin(request)) { @@ -288,10 +290,12 @@ export class DiscordServerService { done(); } + @bindThis private getUserToken(request: FastifyRequest): string | null { return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; } + @bindThis private compareOrigin(request: FastifyRequest): boolean { function normalizeUrl(url?: string): string { return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; diff --git a/packages/backend/src/server/api/integration/GithubServerService.ts b/packages/backend/src/server/api/integration/GithubServerService.ts index 2fd20bf831..3aa04f72ee 100644 --- a/packages/backend/src/server/api/integration/GithubServerService.ts +++ b/packages/backend/src/server/api/integration/GithubServerService.ts @@ -14,6 +14,7 @@ import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { SigninService } from '../SigninService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GithubServerService { @@ -36,9 +37,10 @@ export class GithubServerService { private metaService: MetaService, private signinService: SigninService, ) { - this.create = this.create.bind(this); + //this.create = this.create.bind(this); } + @bindThis public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/disconnect/github', async (request, reply) => { if (!this.compareOrigin(request)) { @@ -260,10 +262,12 @@ export class GithubServerService { done(); } + @bindThis private getUserToken(request: FastifyRequest): string | null { return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; } + @bindThis private compareOrigin(request: FastifyRequest): boolean { function normalizeUrl(url?: string): string { return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts index a8447f9d49..7a127fa29a 100644 --- a/packages/backend/src/server/api/integration/TwitterServerService.ts +++ b/packages/backend/src/server/api/integration/TwitterServerService.ts @@ -14,6 +14,7 @@ import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { SigninService } from '../SigninService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class TwitterServerService { @@ -36,9 +37,10 @@ export class TwitterServerService { private metaService: MetaService, private signinService: SigninService, ) { - this.create = this.create.bind(this); + //this.create = this.create.bind(this); } + @bindThis public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/disconnect/twitter', async (request, reply) => { if (!this.compareOrigin(request)) { @@ -205,10 +207,12 @@ export class TwitterServerService { done(); } + @bindThis private getUserToken(request: FastifyRequest): string | null { return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; } + @bindThis private compareOrigin(request: FastifyRequest): boolean { function normalizeUrl(url?: string): string { return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index d6005b1ee8..198fc190d4 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -15,6 +15,7 @@ import { MessagingChannelService } from './channels/messaging.js'; import { MessagingIndexChannelService } from './channels/messaging-index.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ChannelsService { @@ -37,6 +38,7 @@ export class ChannelsService { ) { } + @bindThis public getChannelService(name: string) { switch (name) { case 'main': return this.mainChannelService; diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 5480c12c09..3e67880b45 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -1,3 +1,4 @@ +import { bindThis } from '@/decorators.js'; import type Connection from '.'; /** @@ -43,6 +44,7 @@ export default abstract class Channel { this.connection = connection; } + @bindThis public send(typeOrPayload: any, payload?: any) { const type = payload === undefined ? typeOrPayload.type : typeOrPayload; const body = payload === undefined ? typeOrPayload.body : payload; diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 8c3c0d2adf..210e016a7e 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class AdminChannel extends Channel { @@ -6,6 +7,7 @@ class AdminChannel extends Channel { public static shouldShare = true; public static requireCredential = true; + @bindThis public async init(params: any) { // Subscribe admin stream this.subscriber.on(`adminStream:${this.user!.id}`, data => { @@ -23,6 +25,7 @@ export class AdminChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): AdminChannel { return new AdminChannel( id, diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 7c34aef495..44beef2da2 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/index.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; import type { StreamMessages } from '../types.js'; @@ -18,9 +19,10 @@ class AntennaChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onEvent = this.onEvent.bind(this); + //this.onEvent = this.onEvent.bind(this); } + @bindThis public async init(params: any) { this.antennaId = params.antennaId as string; @@ -28,6 +30,7 @@ class AntennaChannel extends Channel { this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); } + @bindThis private async onEvent(data: StreamMessages['antenna']['payload']) { if (data.type === 'note') { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); @@ -45,6 +48,7 @@ class AntennaChannel extends Channel { } } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent); @@ -61,6 +65,7 @@ export class AntennaChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): AntennaChannel { return new AntennaChannel( this.noteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 2ef70e62e9..5ba84e43c4 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js'; import type { Packed } from '@/misc/schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; import type { StreamMessages } from '../types.js'; @@ -24,10 +25,11 @@ class ChannelChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); - this.emitTypers = this.emitTypers.bind(this); + //this.onNote = this.onNote.bind(this); + //this.emitTypers = this.emitTypers.bind(this); } + @bindThis public async init(params: any) { this.channelId = params.channelId as string; @@ -37,6 +39,7 @@ class ChannelChannel extends Channel { this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); } + @bindThis private async onNote(note: Packed<'Note'>) { if (note.channelId !== this.channelId) return; @@ -63,6 +66,7 @@ class ChannelChannel extends Channel { this.send('note', note); } + @bindThis private onEvent(data: StreamMessages['channel']['payload']) { if (data.type === 'typing') { const id = data.body; @@ -74,6 +78,7 @@ class ChannelChannel extends Channel { } } + @bindThis private async emitTypers() { const now = new Date(); @@ -90,6 +95,7 @@ class ChannelChannel extends Channel { }); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -110,6 +116,7 @@ export class ChannelChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): ChannelChannel { return new ChannelChannel( this.noteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 80d83cd690..cfcb125b6b 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class DriveChannel extends Channel { @@ -6,6 +7,7 @@ class DriveChannel extends Channel { public static shouldShare = true; public static requireCredential = true; + @bindThis public async init(params: any) { // Subscribe drive stream this.subscriber.on(`driveStream:${this.user!.id}`, data => { @@ -23,6 +25,7 @@ export class DriveChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): DriveChannel { return new DriveChannel( id, 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 a8617582dc..34f782e580 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -6,6 +6,7 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class GlobalTimelineChannel extends Channel { @@ -21,9 +22,10 @@ class GlobalTimelineChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any) { const meta = await this.metaService.fetch(); if (meta.disableGlobalTimeline) { @@ -34,6 +36,7 @@ class GlobalTimelineChannel extends Channel { this.subscriber.on('notesStream', this.onNote); } + @bindThis private async onNote(note: Packed<'Note'>) { if (note.visibility !== 'public') return; if (note.channelId != null) return; @@ -78,6 +81,7 @@ class GlobalTimelineChannel extends Channel { this.send('note', note); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -95,6 +99,7 @@ export class GlobalTimelineChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): GlobalTimelineChannel { return new GlobalTimelineChannel( this.metaService, diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 0f6c081c12..073b737079 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -4,6 +4,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class HashtagChannel extends Channel { @@ -19,9 +20,10 @@ class HashtagChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any) { this.q = params.q; @@ -31,6 +33,7 @@ class HashtagChannel extends Channel { this.subscriber.on('notesStream', this.onNote); } + @bindThis private async onNote(note: Packed<'Note'>) { const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : []; const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); @@ -53,6 +56,7 @@ class HashtagChannel extends Channel { this.send('note', note); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -69,6 +73,7 @@ export class HashtagChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): HashtagChannel { return new HashtagChannel( this.noteEntityService, 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 16e0cebc72..5707ddd821 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -5,6 +5,7 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class HomeTimelineChannel extends Channel { @@ -19,14 +20,16 @@ class HomeTimelineChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any) { // Subscribe events this.subscriber.on('notesStream', this.onNote); } + @bindThis private async onNote(note: Packed<'Note'>) { if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; @@ -85,6 +88,7 @@ class HomeTimelineChannel extends Channel { this.send('note', note); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -101,6 +105,7 @@ export class HomeTimelineChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): HomeTimelineChannel { return new HomeTimelineChannel( this.noteEntityService, 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 f1ce822583..6c6afb12bf 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -7,6 +7,7 @@ import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class HybridTimelineChannel extends Channel { @@ -22,9 +23,10 @@ class HybridTimelineChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any): Promise { const meta = await this.metaService.fetch(); if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; @@ -33,6 +35,7 @@ class HybridTimelineChannel extends Channel { this.subscriber.on('notesStream', this.onNote); } + @bindThis private async onNote(note: Packed<'Note'>) { // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または @@ -95,6 +98,7 @@ class HybridTimelineChannel extends Channel { this.send('note', note); } + @bindThis public dispose(): void { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -112,6 +116,7 @@ export class HybridTimelineChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): HybridTimelineChannel { return new HybridTimelineChannel( this.metaService, 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 5a5a43f845..54388787ef 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -5,6 +5,7 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class LocalTimelineChannel extends Channel { @@ -20,9 +21,10 @@ class LocalTimelineChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onNote = this.onNote.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any) { const meta = await this.metaService.fetch(); if (meta.disableLocalTimeline) { @@ -33,6 +35,7 @@ class LocalTimelineChannel extends Channel { this.subscriber.on('notesStream', this.onNote); } + @bindThis private async onNote(note: Packed<'Note'>) { if (note.user.host !== null) return; if (note.visibility !== 'public') return; @@ -75,6 +78,7 @@ class LocalTimelineChannel extends Channel { this.send('note', note); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); @@ -92,6 +96,7 @@ export class LocalTimelineChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): LocalTimelineChannel { return new LocalTimelineChannel( this.metaService, diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 12908e07b4..42f255b8fe 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/index.js'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class MainChannel extends Channel { @@ -18,6 +19,7 @@ class MainChannel extends Channel { super(id, connection); } + @bindThis public async init(params: any) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { @@ -66,6 +68,7 @@ export class MainChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): MainChannel { return new MainChannel( this.noteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts index bebc07f4ad..66cb79f7a7 100644 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ b/packages/backend/src/server/api/stream/channels/messaging-index.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class MessagingIndexChannel extends Channel { @@ -6,6 +7,7 @@ class MessagingIndexChannel extends Channel { public static shouldShare = true; public static requireCredential = true; + @bindThis public async init(params: any) { // Subscribe messaging index stream this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => { @@ -23,6 +25,7 @@ export class MessagingIndexChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): MessagingIndexChannel { return new MessagingIndexChannel( id, diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index b6ce6c217e..92af6b591c 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -5,6 +5,7 @@ import type { UserGroup } from '@/models/entities/UserGroup.js'; import { MessagingService } from '@/core/MessagingService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; import type { StreamMessages } from '../types.js'; @@ -31,11 +32,12 @@ class MessagingChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.onEvent = this.onEvent.bind(this); - this.onMessage = this.onMessage.bind(this); - this.emitTypers = this.emitTypers.bind(this); + //this.onEvent = this.onEvent.bind(this); + //this.onMessage = this.onMessage.bind(this); + //this.emitTypers = this.emitTypers.bind(this); } + @bindThis public async init(params: any) { this.otherpartyId = params.otherparty; this.otherparty = this.otherpartyId ? await this.usersRepository.findOneByOrFail({ id: this.otherpartyId }) : null; @@ -63,6 +65,7 @@ class MessagingChannel extends Channel { this.subscriber.on(this.subCh, this.onEvent); } + @bindThis private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) { if (data.type === 'typing') { const id = data.body; @@ -76,6 +79,7 @@ class MessagingChannel extends Channel { } } + @bindThis public onMessage(type: string, body: any) { switch (type) { case 'read': @@ -95,6 +99,7 @@ class MessagingChannel extends Channel { } } + @bindThis private async emitTypers() { const now = new Date(); @@ -111,6 +116,7 @@ class MessagingChannel extends Channel { }); } + @bindThis public dispose() { this.subscriber.off(this.subCh, this.onEvent); @@ -138,6 +144,7 @@ export class MessagingChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): MessagingChannel { return new MessagingChannel( this.usersRepository, 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 1802c6723b..c773916103 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,5 +1,6 @@ import Xev from 'xev'; import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; const ev = new Xev(); @@ -11,18 +12,21 @@ class QueueStatsChannel extends Channel { constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onStats = this.onStats.bind(this); - this.onMessage = this.onMessage.bind(this); + //this.onStats = this.onStats.bind(this); + //this.onMessage = this.onMessage.bind(this); } + @bindThis public async init(params: any) { ev.addListener('queueStats', this.onStats); } + @bindThis private onStats(stats: any) { this.send('stats', stats); } + @bindThis public onMessage(type: string, body: any) { switch (type) { case 'requestLog': @@ -37,6 +41,7 @@ class QueueStatsChannel extends Channel { } } + @bindThis public dispose() { ev.removeListener('queueStats', this.onStats); } @@ -51,6 +56,7 @@ export class QueueStatsChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): QueueStatsChannel { return new QueueStatsChannel( id, 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 e2b00de25f..492912dbe6 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,5 +1,6 @@ import Xev from 'xev'; import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; const ev = new Xev(); @@ -11,18 +12,21 @@ class ServerStatsChannel extends Channel { constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onStats = this.onStats.bind(this); - this.onMessage = this.onMessage.bind(this); + //this.onStats = this.onStats.bind(this); + //this.onMessage = this.onMessage.bind(this); } + @bindThis public async init(params: any) { ev.addListener('serverStats', this.onStats); } + @bindThis private onStats(stats: any) { this.send('stats', stats); } + @bindThis public onMessage(type: string, body: any) { switch (type) { case 'requestLog': @@ -37,6 +41,7 @@ class ServerStatsChannel extends Channel { } } + @bindThis public dispose() { ev.removeListener('serverStats', this.onStats); } @@ -51,6 +56,7 @@ export class ServerStatsChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): ServerStatsChannel { return new ServerStatsChannel( id, 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 f9f0d02558..16af32868c 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; -import type { NotesRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UserListsRepository, NotesRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; class UserListChannel extends Channel { @@ -25,10 +25,11 @@ class UserListChannel extends Channel { connection: Channel['connection'], ) { super(id, connection); - this.updateListUsers = this.updateListUsers.bind(this); - this.onNote = this.onNote.bind(this); + //this.updateListUsers = this.updateListUsers.bind(this); + //this.onNote = this.onNote.bind(this); } + @bindThis public async init(params: any) { this.listId = params.listId as string; @@ -48,6 +49,7 @@ class UserListChannel extends Channel { this.listUsersClock = setInterval(this.updateListUsers, 5000); } + @bindThis private async updateListUsers() { const users = await this.userListJoiningsRepository.find({ where: { @@ -59,6 +61,7 @@ class UserListChannel extends Channel { this.listUsers = users.map(x => x.userId); } + @bindThis private async onNote(note: Packed<'Note'>) { if (!this.listUsers.includes(note.userId)) return; @@ -93,6 +96,7 @@ class UserListChannel extends Channel { this.send('note', note); } + @bindThis public dispose() { // Unsubscribe events this.subscriber.off(`userListStream:${this.listId}`, this.send); @@ -118,6 +122,7 @@ export class UserListChannelService { ) { } + @bindThis public create(id: string, connection: Channel['connection']): UserListChannel { return new UserListChannel( this.userListsRepository, diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 0c5066b736..6763953f9d 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -8,6 +8,7 @@ import type { Packed } from '@/misc/schema.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; import type { ChannelsService } from './ChannelsService.js'; import type * as websocket from 'websocket'; import type { EventEmitter } from 'events'; @@ -52,10 +53,10 @@ export default class Connection { if (user) this.user = user; if (token) this.token = token; - this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this); - this.onUserEvent = this.onUserEvent.bind(this); - this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); - this.onBroadcastMessage = this.onBroadcastMessage.bind(this); + //this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this); + //this.onUserEvent = this.onUserEvent.bind(this); + //this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); + //this.onBroadcastMessage = this.onBroadcastMessage.bind(this); this.wsConnection.on('message', this.onWsConnectionMessage); @@ -74,6 +75,7 @@ export default class Connection { } } + @bindThis private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう switch (data.type) { case 'follow': @@ -119,6 +121,7 @@ export default class Connection { /** * クライアントからメッセージ受信時 */ + @bindThis private async onWsConnectionMessage(data: websocket.Message) { if (data.type !== 'utf8') return; if (data.utf8Data == null) return; @@ -153,10 +156,12 @@ export default class Connection { } } + @bindThis private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) { this.sendMessageToWs(data.type, data.body); } + @bindThis public cacheNote(note: Packed<'Note'>) { const add = (note: Packed<'Note'>) => { const existIndex = this.cachedNotes.findIndex(n => n.id === note.id); @@ -176,6 +181,7 @@ export default class Connection { if (note.renote) add(note.renote); } + @bindThis private readNote(body: any) { const id = body.id; @@ -190,6 +196,7 @@ export default class Connection { } } + @bindThis private onReadNotification(payload: any) { if (!payload.id) return; this.notificationService.readNotification(this.user!.id, [payload.id]); @@ -198,6 +205,7 @@ export default class Connection { /** * 投稿購読要求時 */ + @bindThis private onSubscribeNote(payload: any) { if (!payload.id) return; @@ -215,6 +223,7 @@ export default class Connection { /** * 投稿購読解除要求時 */ + @bindThis private onUnsubscribeNote(payload: any) { if (!payload.id) return; @@ -225,6 +234,7 @@ export default class Connection { } } + @bindThis private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { this.sendMessageToWs('noteUpdated', { id: data.body.id, @@ -236,6 +246,7 @@ export default class Connection { /** * チャンネル接続要求時 */ + @bindThis private onChannelConnectRequested(payload: any) { const { channel, id, params, pong } = payload; this.connectChannel(id, params, channel, pong); @@ -244,6 +255,7 @@ export default class Connection { /** * チャンネル切断要求時 */ + @bindThis private onChannelDisconnectRequested(payload: any) { const { id } = payload; this.disconnectChannel(id); @@ -252,6 +264,7 @@ export default class Connection { /** * クライアントにメッセージ送信 */ + @bindThis public sendMessageToWs(type: string, payload: any) { this.wsConnection.send(JSON.stringify({ type: type, @@ -262,6 +275,7 @@ export default class Connection { /** * チャンネルに接続 */ + @bindThis public connectChannel(id: string, params: any, channel: string, pong = false) { const channelService = this.channelsService.getChannelService(channel); @@ -289,6 +303,7 @@ export default class Connection { * チャンネルから切断 * @param id チャンネルコネクションID */ + @bindThis public disconnectChannel(id: string) { const channel = this.channels.find(c => c.id === id); @@ -302,6 +317,7 @@ export default class Connection { * チャンネルへメッセージ送信要求時 * @param data メッセージ */ + @bindThis private onChannelMessageRequested(data: any) { const channel = this.channels.find(c => c.id === data.id); if (channel != null && channel.onMessage != null) { @@ -309,12 +325,14 @@ export default class Connection { } } + @bindThis private typingOnChannel(channel: ChannelModel['id']) { if (this.user) { this.globalEventService.publishChannelStream(channel, 'typing', this.user.id); } } + @bindThis private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) { if (this.user) { if (param.partner) { @@ -325,6 +343,7 @@ export default class Connection { } } + @bindThis private async updateFollowing() { const followings = await this.followingsRepository.find({ where: { @@ -336,6 +355,7 @@ export default class Connection { this.following = new Set(followings.map(x => x.followeeId)); } + @bindThis private async updateMuting() { const mutings = await this.mutingsRepository.find({ where: { @@ -347,6 +367,7 @@ export default class Connection { this.muting = new Set(mutings.map(x => x.muteeId)); } + @bindThis private async updateBlocking() { // ここでいうBlockingは被Blockingの意 const blockings = await this.blockingsRepository.find({ where: { @@ -358,6 +379,7 @@ export default class Connection { this.blocking = new Set(blockings.map(x => x.blockerId)); } + @bindThis private async updateFollowingChannels() { const followings = await this.channelFollowingsRepository.find({ where: { @@ -369,6 +391,7 @@ export default class Connection { this.followingChannels = new Set(followings.map(x => x.followeeId)); } + @bindThis private async updateUserProfile() { this.userProfile = await this.userProfilesRepository.findOneBy({ userId: this.user!.id, @@ -378,6 +401,7 @@ export default class Connection { /** * ストリームが切れたとき */ + @bindThis public dispose() { for (const c of this.channels.filter(c => c.dispose)) { if (c.dispose) c.dispose(); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 3fcf8b7c0d..727cf92831 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -23,6 +23,7 @@ import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import type { ChannelsRepository, ClipsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { deepClone } from '@/misc/clone.js'; +import { bindThis } from '@/decorators.js'; import manifest from './manifest.json' assert { type: 'json' }; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; @@ -80,9 +81,10 @@ export class ClientServerService { @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { - this.createServer = this.createServer.bind(this); + //this.createServer = this.createServer.bind(this); } + @bindThis private async manifestHandler(reply: FastifyReply) { const res = deepClone(manifest); @@ -96,6 +98,7 @@ export class ClientServerService { return (res); } + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { /* TODO //#region Bull Dashboard diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 1d7d49961d..a14609adf9 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -7,6 +7,7 @@ import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class FeedService { @@ -31,6 +32,7 @@ export class FeedService { ) { } + @bindThis public async packFeed(user: User) { const author = { link: `${this.config.url}/@${user.username}`, diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 69f52cc2f2..69bb232d4a 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -9,6 +9,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class UrlPreviewService { @@ -28,6 +29,7 @@ export class UrlPreviewService { this.logger = this.loggerService.getLogger('url-preview'); } + @bindThis private wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) @@ -39,6 +41,7 @@ export class UrlPreviewService { : null; } + @bindThis public async handle( request: FastifyRequest<{ Querystring: { url: string; lang: string; } }>, reply: FastifyReply, diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index cad4d8af6c..9efed267ea 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -15,6 +15,7 @@ export class MockResolver extends Resolver { }); } + @bindThis public async resolve(value: string | IObject): Promise { if (typeof value !== 'string') return value; -- cgit v1.2.3-freya From 2470afaa2e200fb2fc748e0f8eef5e2c215369b6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Jan 2023 21:02:26 +0900 Subject: Role (#9437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update CHANGELOG.md * wip * wip * wip * Update create.ts * wip * wip * Update CHANGELOG.md * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * wip * wip * Update delete.ts * Update delete.ts * wip * wip * wip * Update account-info.vue * wip * wip * Update settings.vue * Update user-info.vue * wip * Update show-file.ts * Update show-user.ts * wip * wip * Update delete.ts * wip * wip * Update overview.moderators.vue * Create 1673500412259-Role.js * wip * wip * Update roles.vue * 色 * Update roles.vue * integrate silence * wip * wip --- CHANGELOG.md | 8 +- locales/ja-JP.yml | 29 +++ packages/backend/migration/1673500412259-Role.js | 37 ++++ .../backend/migration/1673515526953-RoleColor.js | 11 ++ .../backend/migration/1673522856499-RoleIroiro.js | 13 ++ .../migration/1673524604156-RoleLastUsedAt.js | 13 ++ packages/backend/src/core/CoreModule.ts | 12 ++ .../backend/src/core/CreateSystemUserService.ts | 2 +- packages/backend/src/core/DeleteAccountService.ts | 3 + packages/backend/src/core/DriveService.ts | 19 +- packages/backend/src/core/NoteCreateService.ts | 11 +- packages/backend/src/core/RoleService.ts | 201 +++++++++++++++++++++ packages/backend/src/core/SignupService.ts | 6 +- packages/backend/src/core/UserCacheService.ts | 4 +- .../backend/src/core/entities/RoleEntityService.ts | 80 ++++++++ .../backend/src/core/entities/UserEntityService.ts | 19 +- packages/backend/src/di-symbols.ts | 2 + packages/backend/src/models/RepositoryModule.ts | 18 +- packages/backend/src/models/entities/Meta.ts | 21 +-- packages/backend/src/models/entities/Role.ts | 66 +++++++ .../backend/src/models/entities/RoleAssignment.ts | 42 +++++ packages/backend/src/models/entities/User.ts | 22 +-- packages/backend/src/models/index.ts | 6 + packages/backend/src/postgre.ts | 4 + .../backend/src/server/NodeinfoServerService.ts | 7 +- packages/backend/src/server/api/ApiCallService.ts | 57 +++--- packages/backend/src/server/api/EndpointsModule.ts | 52 ++++-- packages/backend/src/server/api/endpoints.ts | 34 ++-- .../server/api/endpoints/admin/accounts/create.ts | 2 +- .../server/api/endpoints/admin/accounts/delete.ts | 10 +- .../endpoints/admin/delete-all-files-of-a-user.ts | 2 +- .../api/endpoints/admin/drive-capacity-override.ts | 61 ------- .../server/api/endpoints/admin/drive/show-file.ts | 9 +- .../server/api/endpoints/admin/get-index-stats.ts | 2 +- .../server/api/endpoints/admin/get-table-stats.ts | 2 +- .../src/server/api/endpoints/admin/get-user-ips.ts | 2 +- .../backend/src/server/api/endpoints/admin/meta.ts | 9 +- .../server/api/endpoints/admin/moderators/add.ts | 49 ----- .../api/endpoints/admin/moderators/remove.ts | 45 ----- .../server/api/endpoints/admin/reset-password.ts | 4 +- .../src/server/api/endpoints/admin/roles/assign.ts | 96 ++++++++++ .../src/server/api/endpoints/admin/roles/create.ts | 75 ++++++++ .../src/server/api/endpoints/admin/roles/delete.ts | 53 ++++++ .../src/server/api/endpoints/admin/roles/list.ts | 39 ++++ .../src/server/api/endpoints/admin/roles/show.ts | 50 +++++ .../server/api/endpoints/admin/roles/unassign.ts | 101 +++++++++++ .../admin/roles/update-default-role-override.ts | 42 +++++ .../src/server/api/endpoints/admin/roles/update.ts | 82 +++++++++ .../src/server/api/endpoints/admin/show-user.ts | 21 ++- .../src/server/api/endpoints/admin/show-users.ts | 26 ++- .../src/server/api/endpoints/admin/silence-user.ts | 55 ------ .../src/server/api/endpoints/admin/suspend-user.ts | 10 +- .../server/api/endpoints/admin/unsilence-user.ts | 51 ------ .../src/server/api/endpoints/admin/update-meta.ts | 15 -- .../src/server/api/endpoints/antennas/create.ts | 4 +- packages/backend/src/server/api/endpoints/drive.ts | 6 +- .../src/server/api/endpoints/drive/files/delete.ts | 4 +- .../src/server/api/endpoints/drive/files/show.ts | 4 +- .../src/server/api/endpoints/drive/files/update.ts | 4 +- packages/backend/src/server/api/endpoints/meta.ts | 20 +- .../src/server/api/endpoints/notes/delete.ts | 6 +- .../server/api/endpoints/notes/global-timeline.ts | 10 +- .../server/api/endpoints/notes/hybrid-timeline.ts | 6 +- .../server/api/endpoints/notes/local-timeline.ts | 10 +- packages/backend/src/server/api/endpoints/users.ts | 5 +- .../src/server/api/endpoints/users/report-abuse.ts | 14 +- .../backend/src/server/api/endpoints/users/show.ts | 8 +- .../server/api/stream/channels/global-timeline.ts | 10 +- .../server/api/stream/channels/hybrid-timeline.ts | 8 +- .../server/api/stream/channels/local-timeline.ts | 10 +- packages/backend/src/server/api/stream/types.ts | 34 ++-- .../backend/src/server/web/ClientServerService.ts | 9 +- packages/backend/src/server/web/boot.js | 4 + packages/frontend/src/components/MkRolePreview.vue | 32 ++++ .../frontend/src/components/MkUserCardMini.vue | 2 +- packages/frontend/src/directives/adaptive-bg.ts | 24 +++ packages/frontend/src/directives/index.ts | 2 + packages/frontend/src/pages/admin/index.vue | 5 + packages/frontend/src/pages/admin/roles.edit.vue | 65 +++++++ packages/frontend/src/pages/admin/roles.editor.vue | 193 ++++++++++++++++++++ packages/frontend/src/pages/admin/roles.role.vue | 121 +++++++++++++ packages/frontend/src/pages/admin/roles.vue | 115 ++++++++++++ packages/frontend/src/pages/admin/settings.vue | 35 +--- packages/frontend/src/pages/admin/users.vue | 1 - packages/frontend/src/pages/timeline.vue | 4 +- packages/frontend/src/pages/user-info.vue | 121 +++++++------ packages/frontend/src/pages/user/home.vue | 4 +- packages/frontend/src/router.ts | 17 ++ packages/frontend/src/scripts/get-user-menu.ts | 32 +--- packages/frontend/src/ui/deck/tl-column.vue | 4 +- 90 files changed, 2027 insertions(+), 638 deletions(-) create mode 100644 packages/backend/migration/1673500412259-Role.js create mode 100644 packages/backend/migration/1673515526953-RoleColor.js create mode 100644 packages/backend/migration/1673522856499-RoleIroiro.js create mode 100644 packages/backend/migration/1673524604156-RoleLastUsedAt.js create mode 100644 packages/backend/src/core/RoleService.ts create mode 100644 packages/backend/src/core/entities/RoleEntityService.ts create mode 100644 packages/backend/src/models/entities/Role.ts create mode 100644 packages/backend/src/models/entities/RoleAssignment.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/add.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/remove.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/assign.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/create.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/list.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/show.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/unassign.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/silence-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/unsilence-user.ts create mode 100644 packages/frontend/src/components/MkRolePreview.vue create mode 100644 packages/frontend/src/directives/adaptive-bg.ts create mode 100644 packages/frontend/src/pages/admin/roles.edit.vue create mode 100644 packages/frontend/src/pages/admin/roles.editor.vue create mode 100644 packages/frontend/src/pages/admin/roles.role.vue create mode 100644 packages/frontend/src/pages/admin/roles.vue (limited to 'packages/backend/src/core/CreateSystemUserService.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 066faa7e05..256df462d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ You should also include the user name that made the change. ## 13.0.0 (unreleased) ### TL;DR -- New features (Play, new widgets, new charts, 🍪👈, etc) +- New features (Role system, Misskey Play, New widgets, New charts, 🍪👈, etc) - Rewriten backend - Better performance (backend and frontend) - Various usability improvements @@ -27,6 +27,11 @@ You should also include the user name that made the change. - 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます - Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator - You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic +- 従来のモデレーターフラグは廃止され、より高度なロール機能が導入されました + - これに伴い、アップデートを行うと全てのモデレーターフラグは失われます。そのため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。 + - サイレンスはロールに統合されました + - ユーザーごとのドライブ容量設定はロールに統合されました + - LTL/GTLの解放状態はロールに統合されました #### For users - ノートのウォッチ機能が削除されました @@ -52,6 +57,7 @@ You should also include the user name that made the change. - API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました ### Improvements +- Role system @syuilo - Misskey Play @syuilo - Introduce retention-rate aggregation @syuilo - Make possible to export favorited notes @syuilo diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index aac89d8fe4..3dd770c60f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -924,6 +924,35 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Misskeyを気に入っていただけましたか?" pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" +roles: "ロール" +role: "ロール" +noramlUser: "一般ユーザー" +undefined: "未定義" +assign: "アサイン" +unassign: "アサインを解除" +color: "色" + +_role: + new: "ロールの作成" + edit: "ロールの編集" + name: "ロール名" + description: "ロールの説明" + type: "ロールの種類" + descriptionOfType: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。" + isPublic: "ロールを公開" + descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" + options: "オプション" + baseRole: "ベースロール" + useBaseValue: "ベースロールの値を使用" + chooseRoleToAssign: "アサインするロールを選択" + canEditMembersByModerator: "モデレーターのメンバー編集を許可" + descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" + _options: + gtlAvailable: "グローバルタイムラインの閲覧" + ltlAvailable: "ローカルタイムラインの閲覧" + canPublicNote: "パブリック投稿の許可" + driveCapacity: "ドライブ容量" + antennaMax: "アンテナの作成可能数" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js new file mode 100644 index 0000000000..a8acedf5b7 --- /dev/null +++ b/packages/backend/migration/1673500412259-Role.js @@ -0,0 +1,37 @@ +export class Role1673500412259 { + name = 'Role1673500412259' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "role" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isAdministrator" boolean NOT NULL DEFAULT false, "options" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id")); COMMENT ON COLUMN "role"."createdAt" IS 'The created date of the Role.'; COMMENT ON COLUMN "role"."updatedAt" IS 'The updated date of the Role.'`); + await queryRunner.query(`CREATE TABLE "role_assignment" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "roleId" character varying(32) NOT NULL, CONSTRAINT "PK_7e79671a8a5db18936173148cb4" PRIMARY KEY ("id")); COMMENT ON COLUMN "role_assignment"."createdAt" IS 'The created date of the RoleAssignment.'; COMMENT ON COLUMN "role_assignment"."userId" IS 'The user ID.'; COMMENT ON COLUMN "role_assignment"."roleId" IS 'The role ID.'`); + await queryRunner.query(`CREATE INDEX "IDX_db5b72c16227c97ca88734d5c2" ON "role_assignment" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_f0de67fd09cd3cd0aabca79994" ON "role_assignment" ("roleId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0953deda7ce6e1448e935859e5" ON "role_assignment" ("userId", "roleId") `); + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isAdmin" TO "isRoot"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isModerator"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableLocalTimeline"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableGlobalTimeline"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "localDriveCapacityMb"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "defaultRoleOverride" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d" FOREIGN KEY ("roleId") REFERENCES "role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d"`); + await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultRoleOverride"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "localDriveCapacityMb" integer NOT NULL DEFAULT '1024'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "disableGlobalTimeline" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "disableLocalTimeline" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`); + await queryRunner.query(`ALTER TABLE "user" ADD "isModerator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isRoot" TO "isAdmin"`); + await queryRunner.query(`DROP INDEX "public"."IDX_0953deda7ce6e1448e935859e5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f0de67fd09cd3cd0aabca79994"`); + await queryRunner.query(`DROP INDEX "public"."IDX_db5b72c16227c97ca88734d5c2"`); + await queryRunner.query(`DROP TABLE "role_assignment"`); + await queryRunner.query(`DROP TABLE "role"`); + } +} diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js new file mode 100644 index 0000000000..343eedf346 --- /dev/null +++ b/packages/backend/migration/1673515526953-RoleColor.js @@ -0,0 +1,11 @@ +export class RoleColor1673515526953 { + name = 'RoleColor1673515526953' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "color" character varying(256)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "color"`); + } +} diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js new file mode 100644 index 0000000000..a1e64d49fe --- /dev/null +++ b/packages/backend/migration/1673522856499-RoleIroiro.js @@ -0,0 +1,13 @@ +export class RoleIroiro1673522856499 { + name = 'RoleIroiro1673522856499' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isSilenced"`); + await queryRunner.query(`ALTER TABLE "role" ADD "canEditMembersByModerator" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "canEditMembersByModerator"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isSilenced" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js new file mode 100644 index 0000000000..786ef07f5e --- /dev/null +++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js @@ -0,0 +1,13 @@ +export class RoleLastUsedAt1673524604156 { + name = 'RoleLastUsedAt1673524604156' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "lastUsedAt"`); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 2f17fa389a..0ae1ee32b2 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -35,6 +35,7 @@ import { PushNotificationService } from './PushNotificationService.js'; import { QueryService } from './QueryService.js'; import { ReactionService } from './ReactionService.js'; import { RelayService } from './RelayService.js'; +import { RoleService } from './RoleService.js'; import { S3Service } from './S3Service.js'; import { SignupService } from './SignupService.js'; import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; @@ -97,6 +98,7 @@ import { UserGroupInvitationEntityService } from './entities/UserGroupInvitation import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; +import { RoleEntityService } from './entities/RoleEntityService.js'; import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -158,6 +160,7 @@ const $PushNotificationService: Provider = { provide: 'PushNotificationService', const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService }; +const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; @@ -220,6 +223,7 @@ const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitat const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; +const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -283,6 +287,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueryService, ReactionService, RelayService, + RoleService, S3Service, SignupService, TwoFactorAuthenticationService, @@ -344,6 +349,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListEntityService, FlashEntityService, FlashLikeEntityService, + RoleEntityService, ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -402,6 +408,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $QueryService, $ReactionService, $RelayService, + $RoleService, $S3Service, $SignupService, $TwoFactorAuthenticationService, @@ -463,6 +470,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, + $RoleEntityService, $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -522,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueryService, ReactionService, RelayService, + RoleService, S3Service, SignupService, TwoFactorAuthenticationService, @@ -582,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListEntityService, FlashEntityService, FlashLikeEntityService, + RoleEntityService, ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -640,6 +650,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $QueryService, $ReactionService, $RelayService, + $RoleService, $S3Service, $SignupService, $TwoFactorAuthenticationService, @@ -700,6 +711,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, + $RoleEntityService, $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 1e753f65cc..8f887d90f9 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -53,7 +53,7 @@ export class CreateSystemUserService { usernameLower: username.toLowerCase(), host: null, token: secret, - isAdmin: false, + isRoot: false, isLocked: true, isExplorable: false, isBot: true, diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index e42c738707..0ac12857c9 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -23,6 +23,9 @@ export class DeleteAccountService { id: string; host: string | null; }): Promise { + const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); + if (_user.isRoot) throw new Error('cannot delete a root account'); + // 物理削除する前にDelete activityを送信する await this.userSuspendService.doPostSuspend(user).catch(e => {}); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index bbdb5fae83..5954abba91 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -32,11 +32,12 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import type S3 from 'aws-sdk/clients/s3.js'; type AddFileArgs = { /** User who wish to add file */ - user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; + user: { id: User['id']; host: User['host'] } | null; /** File path */ path: string; /** Name */ @@ -62,7 +63,7 @@ type AddFileArgs = { type UploadFromUrlArgs = { url: string; - user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; + user: { id: User['id']; host: User['host'] } | null; folderId?: DriveFolder['id'] | null; uri?: string | null; sensitive?: boolean; @@ -106,6 +107,7 @@ export class DriveService { private videoProcessingService: VideoProcessingService, private globalEventService: GlobalEventService, private queueService: QueueService, + private roleService: RoleService, private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, @@ -463,15 +465,16 @@ export class DriveService { //#region Check drive usage if (user && !isLink) { const usage = await this.driveFileEntityService.calcDriveUsageOf(user); - const u = await this.usersRepository.findOneBy({ id: user.id }); - const instance = await this.metaService.fetch(); - let driveCapacity = 1024 * 1024 * (this.userEntityService.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); - - if (this.userEntityService.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { - driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; + let driveCapacity: number; + if (this.userEntityService.isLocalUser(user)) { + const role = await this.roleService.getUserRoleOptions(user.id); + driveCapacity = 1024 * 1024 * role.driveCapacityMb; this.registerLogger.debug('drive capacity override applied'); this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + } else { + const instance = await this.metaService.fetch(); + driveCapacity = 1024 * 1024 * instance.remoteDriveCapacityMb; } this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 6038840406..1c2add5d64 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -42,6 +42,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { RoleService } from '@/core/RoleService.js'; const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -186,6 +187,7 @@ export class NoteCreateService { private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, + private roleService: RoleService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, @@ -197,7 +199,6 @@ export class NoteCreateService { id: User['id']; username: User['username']; host: User['host']; - isSilenced: User['isSilenced']; createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent = false): Promise { @@ -224,9 +225,10 @@ export class NoteCreateService { if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - // サイレンス - if (user.isSilenced && data.visibility === 'public' && data.channel == null) { - data.visibility = 'home'; + if (data.visibility === 'public' && data.channel == null) { + if ((await this.roleService.getUserRoleOptions(user.id)).canPublicNote) { + data.visibility = 'home'; + } } // Renote対象が「ホームまたは全体」以外の公開範囲ならreject @@ -418,7 +420,6 @@ export class NoteCreateService { id: User['id']; username: User['username']; host: User['host']; - isSilenced: User['isSilenced']; createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts new file mode 100644 index 0000000000..6ce7f431ca --- /dev/null +++ b/packages/backend/src/core/RoleService.ts @@ -0,0 +1,201 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import { In } from 'typeorm'; +import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { Cache } from '@/misc/cache.js'; +import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +export type RoleOptions = { + gtlAvailable: boolean; + ltlAvailable: boolean; + canPublicNote: boolean; + driveCapacityMb: number; + antennaLimit: number; +}; + +export const DEFAULT_ROLE: RoleOptions = { + gtlAvailable: true, + ltlAvailable: true, + canPublicNote: true, + driveCapacityMb: 100, + antennaLimit: 5, +}; + +@Injectable() +export class RoleService implements OnApplicationShutdown { + private rolesCache: Cache; + private roleAssignmentByUserIdCache: Cache; + + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private metaService: MetaService, + ) { + //this.onMessage = this.onMessage.bind(this); + + this.rolesCache = new Cache(Infinity); + this.roleAssignmentByUserIdCache = new Cache(Infinity); + + this.redisSubscriber.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message; + switch (type) { + case 'roleCreated': { + const cached = this.rolesCache.get(null); + if (cached) { + body.createdAt = new Date(body.createdAt); + body.updatedAt = new Date(body.updatedAt); + body.lastUsedAt = new Date(body.lastUsedAt); + cached.push(body); + } + break; + } + case 'roleUpdated': { + const cached = this.rolesCache.get(null); + if (cached) { + const i = cached.findIndex(x => x.id === body.id); + if (i > -1) { + body.createdAt = new Date(body.createdAt); + body.updatedAt = new Date(body.updatedAt); + body.lastUsedAt = new Date(body.lastUsedAt); + cached[i] = body; + } + } + break; + } + case 'roleDeleted': { + const cached = this.rolesCache.get(null); + if (cached) { + this.rolesCache.set(null, cached.filter(x => x.id !== body.id)); + } + break; + } + case 'userRoleAssigned': { + const cached = this.roleAssignmentByUserIdCache.get(body.userId); + if (cached) { + body.createdAt = new Date(body.createdAt); + cached.push(body); + } + break; + } + case 'userRoleUnassigned': { + const cached = this.roleAssignmentByUserIdCache.get(body.userId); + if (cached) { + this.roleAssignmentByUserIdCache.set(body.userId, cached.filter(x => x.id !== body.id)); + } + break; + } + default: + break; + } + } + } + + @bindThis + public async getUserRoles(userId: User['id']) { + const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const assignedRoleIds = assigns.map(x => x.roleId); + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + return roles.filter(r => assignedRoleIds.includes(r.id)); + } + + @bindThis + public async getUserRoleOptions(userId: User['id'] | null): Promise { + const meta = await this.metaService.fetch(); + const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; + + if (userId == null) return baseRoleOptions; + + const roles = await this.getUserRoles(userId); + + function getOptionValues(option: keyof RoleOptions) { + if (roles.length === 0) return [baseRoleOptions[option]]; + return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]); + } + + return { + gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true), + ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true), + canPublicNote: getOptionValues('canPublicNote').some(x => x === true), + driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')), + antennaLimit: Math.max(...getOptionValues('antennaLimit')), + }; + } + + @bindThis + public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + if (user == null) return false; + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); + } + + @bindThis + public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + if (user == null) return false; + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); + } + + @bindThis + public async getModeratorIds(includeAdmins = true): Promise { + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); + const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(moderatorRoles.map(r => r.id)), + }) : []; + // TODO: isRootなアカウントも含める + return assigns.map(a => a.userId); + } + + @bindThis + public async getModerators(includeAdmins = true): Promise { + const ids = await this.getModeratorIds(includeAdmins); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public async getAdministratorIds(): Promise { + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const administratorRoles = roles.filter(r => r.isAdministrator); + const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(administratorRoles.map(r => r.id)), + }) : []; + // TODO: isRootなアカウントも含める + return assigns.map(a => a.userId); + } + + @bindThis + public async getAdministrators(): Promise { + const ids = await this.getAdministratorIds(); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 9cf203566d..90a7186909 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -11,10 +11,10 @@ import { IdService } from '@/core/IdService.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; -import UsersChart from './chart/charts/users.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { UtilityService } from './UtilityService.js'; import { bindThis } from '@/decorators.js'; +import UsersChart from './chart/charts/users.js'; +import { UtilityService } from './UtilityService.js'; @Injectable() export class SignupService { @@ -112,7 +112,7 @@ export class SignupService { usernameLower: username.toLowerCase(), host: this.utilityService.toPunyNullable(host), token: secret, - isAdmin: (await this.usersRepository.countBy({ + isRoot: (await this.usersRepository.countBy({ host: IsNull(), })) === 0, })); diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 423c8993e3..3f735c0c53 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -5,8 +5,8 @@ import { Cache } from '@/misc/cache.js'; import type { CacheableLocalUser, CacheableUser, ILocalUser } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { @@ -42,8 +42,6 @@ export class UserCacheService implements OnApplicationShutdown { const { type, body } = obj.message; switch (type) { case 'userChangeSuspendedState': - case 'userChangeSilencedState': - case 'userChangeModeratorState': case 'remoteUserUpdated': { const user = await this.usersRepository.findOneByOrFail({ id: body.id }); this.userByIdCache.set(user.id, user); diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts new file mode 100644 index 0000000000..22c4cdff81 --- /dev/null +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -0,0 +1,80 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { User } from '@/models/entities/User.js'; +import type { Role } from '@/models/entities/Role.js'; +import { bindThis } from '@/decorators.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class RoleEntityService { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private userEntityService: UserEntityService, + ) { + } + + @bindThis + public async pack( + src: Role['id'] | Role, + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: boolean; + }, + ) { + const opts = Object.assign({ + detail: true, + }, options); + + const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); + + const assigns = await this.roleAssignmentsRepository.findBy({ + roleId: role.id, + }); + + const roleOptions = { ...role.options }; + for (const [k, v] of Object.entries(DEFAULT_ROLE)) { + if (roleOptions[k] == null) roleOptions[k] = { + useDefault: true, + value: v, + }; + } + + return await awaitAll({ + id: role.id, + createdAt: role.createdAt.toISOString(), + updatedAt: role.updatedAt.toISOString(), + name: role.name, + description: role.description, + color: role.color, + isPublic: role.isPublic, + isAdministrator: role.isAdministrator, + isModerator: role.isModerator, + canEditMembersByModerator: role.canEditMembersByModerator, + options: roleOptions, + ...(opts.detail ? { + users: this.userEntityService.packMany(assigns.map(x => x.userId), me), + } : {}), + }); + } + + @bindThis + public packMany( + roles: any[], + me: { id: User['id'] }, + options?: { + detail?: boolean; + }, + ) { + return Promise.all(roles.map(x => this.pack(x, me, options))); + } +} + diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a123746220..9a90aec456 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -13,6 +13,8 @@ import type { Instance } from '@/models/entities/Instance.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { AntennaService } from '../AntennaService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -41,7 +43,6 @@ function isRemoteUser(user: T): user is T & { function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); } -import { bindThis } from '@/decorators.js'; @Injectable() export class UserEntityService implements OnModuleInit { @@ -50,6 +51,7 @@ export class UserEntityService implements OnModuleInit { private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; private antennaService: AntennaService; + private roleService: RoleService; private userInstanceCache: Cache; constructor( @@ -120,6 +122,7 @@ export class UserEntityService implements OnModuleInit { //private pageEntityService: PageEntityService, //private customEmojiService: CustomEmojiService, //private antennaService: AntennaService, + //private roleService: RoleService, ) { this.userInstanceCache = new Cache(1000 * 60 * 60 * 3); } @@ -130,6 +133,7 @@ export class UserEntityService implements OnModuleInit { this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.antennaService = this.moduleRef.get('AntennaService'); + this.roleService = this.moduleRef.get('RoleService'); } //#region Validators @@ -383,6 +387,9 @@ export class UserEntityService implements OnModuleInit { (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; + const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; + const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; + const falsy = opts.detail ? false : undefined; const packed = { @@ -392,8 +399,6 @@ export class UserEntityService implements OnModuleInit { host: user.host, avatarUrl: this.getAvatarUrlSync(user), avatarBlurhash: user.avatar?.blurhash ?? null, - isAdmin: user.isAdmin ?? falsy, - isModerator: user.isModerator ?? falsy, isBot: user.isBot ?? falsy, isCat: user.isCat ?? falsy, instance: user.host ? this.userInstanceCache.fetch(user.host, @@ -418,7 +423,7 @@ export class UserEntityService implements OnModuleInit { bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, bannerBlurhash: user.banner?.blurhash ?? null, isLocked: user.isLocked, - isSilenced: user.isSilenced ?? falsy, + isSilenced: this.roleService.getUserRoleOptions(user.id).then(r => !r.canPublicNote), isSuspended: user.isSuspended ?? falsy, description: profile!.description, location: profile!.location, @@ -443,14 +448,13 @@ export class UserEntityService implements OnModuleInit { userId: user.id, }).then(result => result >= 1) : false, - ...(isMe || opts.includeSecrets ? { - driveCapacityOverrideMb: user.driveCapacityOverrideMb, - } : {}), } : {}), ...(opts.detail && isMe ? { avatarId: user.avatarId, bannerId: user.bannerId, + isModerator: isModerator, + isAdmin: isAdmin, injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, alwaysMarkNsfw: profile!.alwaysMarkNsfw, @@ -484,6 +488,7 @@ export class UserEntityService implements OnModuleInit { } : {}), ...(opts.includeSecrets ? { + role: this.roleService.getUserRoleOptions(user.id), email: profile!.email, emailVerified: profile!.emailVerified, securityKeysList: profile!.twoFactorEnabled diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 9719d773ca..3fb0cd4dae 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -69,6 +69,8 @@ export const DI = { adsRepository: Symbol('adsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), + rolesRepository: Symbol('rolesRepository'), + roleAssignmentsRepository: Symbol('roleAssignmentsRepository'), flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), //#endregion diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index a5d5a63931..2a235bc6fc 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -400,6 +400,18 @@ const $flashLikesRepository: Provider = { inject: [DI.db], }; +const $rolesRepository: Provider = { + provide: DI.rolesRepository, + useFactory: (db: DataSource) => db.getRepository(Role), + inject: [DI.db], +}; + +const $roleAssignmentsRepository: Provider = { + provide: DI.roleAssignmentsRepository, + useFactory: (db: DataSource) => db.getRepository(RoleAssignment), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -468,6 +480,8 @@ const $flashLikesRepository: Provider = { $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, + $rolesRepository, + $roleAssignmentsRepository, $flashsRepository, $flashLikesRepository, ], @@ -536,6 +550,8 @@ const $flashLikesRepository: Provider = { $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, + $rolesRepository, + $roleAssignmentsRepository, $flashsRepository, $flashLikesRepository, ], diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index fb25e370d2..0d65a8d17a 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -42,16 +42,6 @@ export class Meta { }) public disableRegistration: boolean; - @Column('boolean', { - default: false, - }) - public disableLocalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public disableGlobalTimeline: boolean; - @Column('boolean', { default: false, }) @@ -227,12 +217,6 @@ export class Meta { }) public enableSensitiveMediaDetectionForVideos: boolean; - @Column('integer', { - default: 1024, - comment: 'Drive capacity of a local user (MB)', - }) - public localDriveCapacityMb: number; - @Column('integer', { default: 32, comment: 'Drive capacity of a remote user (MB)', @@ -476,4 +460,9 @@ export class Meta { default: true, }) public enableActiveEmailValidation: boolean; + + @Column('jsonb', { + default: { }, + }) + public defaultRoleOverride: Record; } diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts new file mode 100644 index 0000000000..34dbc2ce41 --- /dev/null +++ b/packages/backend/src/models/entities/Role.ts @@ -0,0 +1,66 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Role { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Role.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Role.', + }) + public updatedAt: Date; + + @Column('timestamp with time zone', { + comment: 'The last used date of the Role.', + }) + public lastUsedAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 1024, + }) + public description: string; + + @Column('varchar', { + length: 256, nullable: true, + }) + public color: string | null; + + @Column('boolean', { + default: false, + }) + public isPublic: boolean; + + @Column('boolean', { + default: false, + }) + public isModerator: boolean; + + @Column('boolean', { + default: false, + }) + public isAdministrator: boolean; + + @Column('boolean', { + default: false, + }) + public canEditMembersByModerator: boolean; + + @Column('jsonb', { + default: { }, + }) + public options: Record; +} diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts new file mode 100644 index 0000000000..e86f2a8999 --- /dev/null +++ b/packages/backend/src/models/entities/RoleAssignment.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Role } from './Role.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'roleId'], { unique: true }) +export class RoleAssignment { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the RoleAssignment.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The role ID.', + }) + public roleId: Role['id']; + + @ManyToOne(type => Role, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public role: Role | null; +} diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 73736f0150..8bd5c9700d 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -112,12 +112,6 @@ export class User { }) public isSuspended: boolean; - @Column('boolean', { - default: false, - comment: 'Whether the User is silenced.', - }) - public isSilenced: boolean; - @Column('boolean', { default: false, comment: 'Whether the User is locked.', @@ -138,15 +132,9 @@ export class User { @Column('boolean', { default: false, - comment: 'Whether the User is the admin.', - }) - public isAdmin: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether the User is a moderator.', + comment: 'Whether the User is the root.', }) - public isModerator: boolean; + public isRoot: boolean; @Index() @Column('boolean', { @@ -218,12 +206,6 @@ export class User { }) public token: string | null; - @Column('integer', { - nullable: true, - comment: 'Overrides user drive capacity limit', - }) - public driveCapacityOverrideMb: number | null; - constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index b132475747..50697597ad 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; import { Webhook } from '@/models/entities/Webhook.js'; import { Channel } from '@/models/entities/Channel.js'; import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; import { Flash } from '@/models/entities/Flash.js'; import { FlashLike } from '@/models/entities/FlashLike.js'; import type { Repository } from 'typeorm'; @@ -131,6 +133,8 @@ export { Webhook, Channel, RetentionAggregation, + Role, + RoleAssignment, Flash, FlashLike, }; @@ -199,5 +203,7 @@ export type UserSecurityKeysRepository = Repository; export type WebhooksRepository = Repository; export type ChannelsRepository = Repository; export type RetentionAggregationsRepository = Repository; +export type RolesRepository = Repository; +export type RoleAssignmentsRepository = Repository; export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts index 4f6b157d80..c55cb78a6a 100644 --- a/packages/backend/src/postgre.ts +++ b/packages/backend/src/postgre.ts @@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; import { Webhook } from '@/models/entities/Webhook.js'; import { Channel } from '@/models/entities/Channel.js'; import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; import { Flash } from '@/models/entities/Flash.js'; import { FlashLike } from '@/models/entities/FlashLike.js'; @@ -186,6 +188,8 @@ export const entities = [ Webhook, UserIp, RetentionAggregation, + Role, + RoleAssignment, Flash, FlashLike, ...charts, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index c4236c8752..19380d13a4 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; import UsersChart from '@/core/chart/charts/users.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; const nodeinfo2_1path = '/nodeinfo/2.1'; @@ -73,6 +74,8 @@ export class NodeinfoServerService { const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; + const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; + return { software: { name: 'misskey', @@ -102,8 +105,8 @@ export class NodeinfoServerService { repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, - disableLocalTimeline: meta.disableLocalTimeline, - disableGlobalTimeline: meta.disableGlobalTimeline, + disableLocalTimeline: !baseRoleOptions.ltlAvailable, + disableGlobalTimeline: !baseRoleOptions.gtlAvailable, emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 68f43c7dfc..415fbf08dd 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -12,6 +12,7 @@ import type { UserIpsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -41,6 +42,7 @@ export class ApiCallService implements OnApplicationShutdown { private metaService: MetaService, private authenticateService: AuthenticateService, private rateLimiterService: RateLimiterService, + private roleService: RoleService, private apiLoggerService: ApiLoggerService, ) { this.logger = this.apiLoggerService.logger; @@ -202,7 +204,6 @@ export class ApiCallService implements OnApplicationShutdown { request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, ) { const isSecure = user != null && token == null; - const isModerator = user != null && (user.isModerator || user.isAdmin); if (ep.meta.secure && !isSecure) { throw new ApiError(accessDenied); @@ -234,30 +235,40 @@ export class ApiCallService implements OnApplicationShutdown { }); } - if (ep.meta.requireCredential && user == null) { - throw new ApiError({ - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - httpStatusCode: 401, - }); - } - - if (ep.meta.requireCredential && user!.isSuspended) { - throw new ApiError({ - message: 'Your account has been suspended.', - code: 'YOUR_ACCOUNT_SUSPENDED', - id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, - }); - } - - if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); + if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { + if (user == null) { + throw new ApiError({ + message: 'Credential required.', + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + httpStatusCode: 401, + }); + } else if (user!.isSuspended) { + throw new ApiError({ + message: 'Your account has been suspended.', + code: 'YOUR_ACCOUNT_SUSPENDED', + id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', + httpStatusCode: 403, + }); + } } - if (ep.meta.requireModerator && !isModerator) { - throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { + const myRoles = await this.roleService.getUserRoles(user!.id); + if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { + throw new ApiError({ + message: 'You are not assigned to a moderator role.', + code: 'ROLE_PERMISSION_DENIED', + id: 'd33d5333-db36-423d-a8f9-1a2b9549da41', + }); + } + if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) { + throw new ApiError({ + message: 'You are not assigned to an administrator role.', + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); + } } if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ab9349966d..c226c4e93c 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -38,8 +38,6 @@ import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js' import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; import * as ep___admin_invite from './endpoints/admin/invite.js'; -import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; -import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -55,13 +53,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -326,7 +330,6 @@ import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; import * as ep___retention from './endpoints/retention.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -369,8 +372,6 @@ const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', us const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; const $admin_invite: Provider = { provide: 'ep:admin/invite', useClass: ep___admin_invite.default }; -const $admin_moderators_add: Provider = { provide: 'ep:admin/moderators/add', useClass: ep___admin_moderators_add.default }; -const $admin_moderators_remove: Provider = { provide: 'ep:admin/moderators/remove', useClass: ep___admin_moderators_remove.default }; const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; @@ -386,13 +387,19 @@ const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; -const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; -const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default }; const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default }; const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default }; +const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default }; +const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default }; +const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default }; +const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default }; +const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default }; +const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; +const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; +const $admin_roles_updateDefaultRoleOverride: Provider = { provide: 'ep:admin/roles/update-default-role-override', useClass: ep___admin_roles_updateDefaultRoleOverride.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; @@ -656,7 +663,6 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by- const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; -const $admin_driveCapOverride: Provider = { provide: 'ep:admin/drive-capacity-override', useClass: ep___admin_driveCapOverride.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; @@ -704,8 +710,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getTableStats, $admin_getUserIps, $admin_invite, - $admin_moderators_add, - $admin_moderators_remove, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -721,13 +725,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_showModerationLogs, $admin_showUser, $admin_showUsers, - $admin_silenceUser, $admin_suspendUser, - $admin_unsilenceUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultRoleOverride, $announcements, $antennas_create, $antennas_delete, @@ -991,7 +1001,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, - $admin_driveCapOverride, $fetchRss, $retention, ], @@ -1033,8 +1042,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getTableStats, $admin_getUserIps, $admin_invite, - $admin_moderators_add, - $admin_moderators_remove, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -1050,13 +1057,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_showModerationLogs, $admin_showUser, $admin_showUsers, - $admin_silenceUser, $admin_suspendUser, - $admin_unsilenceUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultRoleOverride, $announcements, $antennas_create, $antennas_delete, @@ -1318,7 +1331,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, - $admin_driveCapOverride, $fetchRss, $retention, ], diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f9749ad660..1df3240e41 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -37,8 +37,6 @@ import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js' import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; import * as ep___admin_invite from './endpoints/admin/invite.js'; -import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; -import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -54,13 +52,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -325,7 +329,6 @@ import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; import * as ep___retention from './endpoints/retention.js'; const eps = [ @@ -366,8 +369,6 @@ const eps = [ ['admin/get-table-stats', ep___admin_getTableStats], ['admin/get-user-ips', ep___admin_getUserIps], ['admin/invite', ep___admin_invite], - ['admin/moderators/add', ep___admin_moderators_add], - ['admin/moderators/remove', ep___admin_moderators_remove], ['admin/promo/create', ep___admin_promo_create], ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], @@ -383,13 +384,19 @@ const eps = [ ['admin/show-moderation-logs', ep___admin_showModerationLogs], ['admin/show-user', ep___admin_showUser], ['admin/show-users', ep___admin_showUsers], - ['admin/silence-user', ep___admin_silenceUser], ['admin/suspend-user', ep___admin_suspendUser], - ['admin/unsilence-user', ep___admin_unsilenceUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], ['admin/update-user-note', ep___admin_updateUserNote], + ['admin/roles/create', ep___admin_roles_create], + ['admin/roles/delete', ep___admin_roles_delete], + ['admin/roles/list', ep___admin_roles_list], + ['admin/roles/show', ep___admin_roles_show], + ['admin/roles/update', ep___admin_roles_update], + ['admin/roles/assign', ep___admin_roles_assign], + ['admin/roles/unassign', ep___admin_roles_unassign], + ['admin/roles/update-default-role-override', ep___admin_roles_updateDefaultRoleOverride], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], @@ -653,7 +660,6 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], - ['admin/drive-capacity-override', ep___admin_driveCapOverride], ['fetch-rss', ep___fetchRss], ['retention', ep___retention], ]; @@ -680,14 +686,14 @@ export interface IEndpointMeta { readonly requireCredential?: boolean; /** - * 管理者のみ使えるエンドポイントか否か + * isModeratorなロールを必要とするか */ - readonly requireAdmin?: boolean; + readonly requireModerator?: boolean; /** - * 管理者またはモデレーターのみ使えるエンドポイントか否か + * isAdministratorなロールを必要とするか */ - readonly requireModerator?: boolean; + readonly requireAdmin?: boolean; /** * エンドポイントのリミテーションに関するやつ diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index c76ece9e05..bac8ae16e5 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -47,7 +47,7 @@ export default class extends Endpoint { const noUsers = (await this.usersRepository.countBy({ host: IsNull(), })) === 0; - if (!noUsers && !me?.isAdmin) throw new Error('access denied'); + if (!noUsers && !me?.isRoot) throw new Error('access denied'); const { account, secret } = await this.signupService.signup({ username: ps.username, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index b7081987ca..e9f72676f0 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -11,7 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, } as const; export const paramDef = { @@ -41,12 +41,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); + if (user.isRoot) { + throw new Error('cannot delete a root account'); } if (this.userEntityService.isLocalUser(user)) { diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index 22b78bf19d..c193ed3fb3 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts deleted file mode 100644 index 665e2a8cce..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - overrideMb: { type: 'number', nullable: true }, - }, - required: ['userId', 'overrideMb'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private userEntityService: UserEntityService, - private moderationLogService: ModerationLogService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (!this.userEntityService.isLocalUser(user)) { - throw new Error('user is not local user'); - } - - /*if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - }*/ - - await this.usersRepository.update(user.id, { - driveCapacityOverrideMb: ps.overrideMb, - }); - - this.moderationLogService.insertModerationLog(me, 'change-drive-capacity-override', { - targetId: user.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 6180eeae2b..6376cb153c 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -159,6 +160,8 @@ export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({ @@ -175,6 +178,8 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } + const isModerator = await this.roleService.isModerator(me); + return { id: file.id, userId: file.userId, @@ -202,8 +207,8 @@ export default class extends Endpoint { name: file.name, md5: file.md5, createdAt: file.createdAt.toISOString(), - requestIp: me.isAdmin ? file.requestIp : null, - requestHeaders: me.isAdmin ? file.requestHeaders : null, + requestIp: isModerator ? file.requestIp : null, + requestHeaders: isModerator ? file.requestHeaders : null, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index e53d0bfcea..8ffd2b01e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - requireModerator: true, + requireAdmin: true, tags: ['admin'], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index 41014cb167..09d61bd741 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - requireModerator: true, + requireAdmin: true, tags: ['admin'], 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 947a673def..bfcc8a700b 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 @@ -7,7 +7,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireAdmin: true, + requireModerator: true, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 794ea3d5c9..33f162acf9 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -15,10 +16,6 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - driveCapacityPerLocalUserMb: { - type: 'number', - optional: false, nullable: false, - }, driveCapacityPerRemoteUserMb: { type: 'number', optional: false, nullable: false, @@ -377,9 +374,6 @@ export default class extends Endpoint { repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -451,6 +445,7 @@ export default class extends Endpoint { deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts deleted file mode 100644 index 2fc5a35e8e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot mark as moderator if admin user'); - } - - await this.usersRepository.update(user.id, { - isModerator: true, - }); - - this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts deleted file mode 100644 index f0d7a3f12d..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - await this.usersRepository.update(user.id, { - isModerator: false, - }); - - this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index f7d27be9cb..d263f99f6e 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -50,8 +50,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot reset password of admin'); + if (user.isRoot) { + throw new Error('cannot reset password of root'); } const passwd = rndstr('a-zA-Z0-9', 8); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts new file mode 100644 index 0000000000..7bfb2f6625 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '6503c040-6af4-4ed9-bf07-f2dd16678eab', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '558ea170-f653-4700-94d0-5a818371d0df', + }, + + accessDenied: { + message: 'Only administrators can edit members of the role.', + code: 'ACCESS_DENIED', + id: '25b5bc31-dc79-4ebd-9bd2-c84978fd052c', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + 'userId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private globalEventService: GlobalEventService, + private roleService: RoleService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) { + throw new ApiError(meta.errors.accessDenied); + } + + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const date = new Date(); + const created = await this.roleAssignmentsRepository.insert({ + id: this.idService.genId(), + createdAt: date, + roleId: role.id, + userId: user.id, + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + + this.rolesRepository.update(ps.roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleAssigned', created); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts new file mode 100644 index 0000000000..b04188fac6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -0,0 +1,75 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + color: { type: 'string', nullable: true }, + isPublic: { type: 'boolean' }, + isModerator: { type: 'boolean' }, + isAdministrator: { type: 'boolean' }, + canEditMembersByModerator: { type: 'boolean' }, + options: { + type: 'object', + }, + }, + required: [ + 'name', + 'description', + 'color', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'canEditMembersByModerator', + 'options', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + private idService: IdService, + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.genId(), + createdAt: date, + updatedAt: date, + lastUsedAt: date, + name: ps.name, + description: ps.description, + color: ps.color, + isPublic: ps.isPublic, + isAdministrator: ps.isAdministrator, + isModerator: ps.isModerator, + canEditMembersByModerator: ps.canEditMembersByModerator, + options: ps.options, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); + + return await this.roleEntityService.pack(created, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts new file mode 100644 index 0000000000..b56ebdb3ee --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'de0d6ecd-8e0a-4253-88ff-74bc89ae3d45', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + await this.rolesRepository.delete({ + id: ps.roleId, + }); + this.globalEventService.publishInternalEvent('roleDeleted', role); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts new file mode 100644 index 0000000000..458a8d535b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -0,0 +1,39 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [ + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const roles = await this.rolesRepository.find({ + order: { lastUsedAt: 'DESC' }, + }); + return await this.roleEntityService.packMany(roles, me, { detail: false }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts new file mode 100644 index 0000000000..c83f96191d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + return await this.roleEntityService.pack(role); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts new file mode 100644 index 0000000000..141cc5ee89 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -0,0 +1,101 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '6e519036-a70d-4c76-b679-bc8fb18194e2', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '2b730f78-1179-461b-88ad-d24c9af1a5ce', + }, + + notAssigned: { + message: 'Not assigned.', + code: 'NOT_ASSIGNED', + id: 'b9060ac7-5c94-4da4-9f55-2047c953df44', + }, + + accessDenied: { + message: 'Only administrators can edit members of the role.', + code: 'ACCESS_DENIED', + id: '24636eee-e8c1-493e-94b2-e16ad401e262', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + 'userId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private globalEventService: GlobalEventService, + private roleService: RoleService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) { + throw new ApiError(meta.errors.accessDenied); + } + + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id }); + if (roleAssignment == null) { + throw new ApiError(meta.errors.notAssigned); + } + + await this.roleAssignmentsRepository.delete(roleAssignment.id); + + this.rolesRepository.update(ps.roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts new file mode 100644 index 0000000000..35da04efd2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { MetaService } from '@/core/MetaService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + options: { + type: 'object', + }, + }, + required: [ + 'options', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + private metaService: MetaService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + await this.metaService.update({ + defaultRoleOverride: ps.options, + }); + this.globalEventService.publishInternalEvent('defaultRoleOverrideUpdated', ps.options); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts new file mode 100644 index 0000000000..7d97d68e14 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -0,0 +1,82 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'cd23ef55-09ad-428a-ac61-95a45e124b32', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string' }, + description: { type: 'string' }, + color: { type: 'string', nullable: true }, + isPublic: { type: 'boolean' }, + isModerator: { type: 'boolean' }, + isAdministrator: { type: 'boolean' }, + canEditMembersByModerator: { type: 'boolean' }, + options: { + type: 'object', + }, + }, + required: [ + 'roleId', + 'name', + 'description', + 'color', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'canEditMembersByModerator', + 'options', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const date = new Date(); + await this.rolesRepository.update(ps.roleId, { + updatedAt: date, + name: ps.name, + description: ps.description, + color: ps.color, + isPublic: ps.isPublic, + isModerator: ps.isModerator, + isAdministrator: ps.isAdministrator, + canEditMembersByModerator: ps.canEditMembersByModerator, + options: ps.options, + }); + const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); + this.globalEventService.publishInternalEvent('roleUpdated', updated); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index e4031cf960..3f4ec299af 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; export const meta = { tags: ['admin'], @@ -35,6 +37,9 @@ export default class extends Endpoint { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + + private roleService: RoleService, + private roleEntityService: RoleEntityService, ) { super(meta, paramDef, async (ps, me) => { const [user, profile] = await Promise.all([ @@ -46,15 +51,16 @@ export default class extends Endpoint { throw new Error('user not found'); } + const isModerator = await this.roleService.isModerator(user); + const isSilenced = !(await this.roleService.getUserRoleOptions(user.id)).canPublicNote; + const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); - if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { + if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) { throw new Error('cannot show info of admin'); } - if (!_me.isAdmin) { + if (!await this.roleService.isAdministrator(_me)) { return { - isModerator: user.isModerator, - isSilenced: user.isSilenced, isSuspended: user.isSuspended, }; } @@ -66,6 +72,8 @@ export default class extends Endpoint { const signins = await this.signinsRepository.findBy({ userId: user.id }); + const roles = await this.roleService.getUserRoles(user.id); + return { email: profile.email, emailVerified: profile.emailVerified, @@ -80,12 +88,13 @@ export default class extends Endpoint { mutedWords: profile.mutedWords, mutedInstances: profile.mutedInstances, mutingNotificationTypes: profile.mutingNotificationTypes, - isModerator: user.isModerator, - isSilenced: user.isSilenced, + isModerator: isModerator, + isSilenced: isSilenced, isSuspended: user.isSuspended, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote, signins, + roles: await this.roleEntityService.packMany(roles, me, { detail: false }), }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 5a67cf522a..426973f282 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], @@ -28,7 +29,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] }, - state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, + state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { @@ -49,18 +50,33 @@ export default class extends Endpoint { private usersRepository: UsersRepository, private userEntityService: UserEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const query = this.usersRepository.createQueryBuilder('user'); switch (ps.state) { case 'available': query.where('user.isSuspended = FALSE'); break; - case 'admin': query.where('user.isAdmin = TRUE'); break; - case 'moderator': query.where('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; - case 'silenced': query.where('user.isSilenced = TRUE'); break; case 'suspended': query.where('user.isSuspended = TRUE'); break; + case 'admin': { + const adminIds = await this.roleService.getAdministratorIds(); + if (adminIds.length === 0) return []; + query.where('user.id IN (:...adminIds)', { adminIds: adminIds }); + break; + } + case 'moderator': { + const moderatorIds = await this.roleService.getModeratorIds(false); + if (moderatorIds.length === 0) return []; + query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); + break; + } + case 'adminOrModerator': { + const adminOrModeratorIds = await this.roleService.getModeratorIds(); + if (adminOrModeratorIds.length === 0) return []; + query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); + break; + } } switch (ps.origin) { diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts deleted file mode 100644 index b9dbd211e0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot silence admin'); - } - - await this.usersRepository.update(user.id, { - isSilenced: true, - }); - - this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true }); - - this.moderationLogService.insertModerationLog(me, 'silence', { - targetId: user.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 9fc1391570..3ad6c7c484 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -9,6 +9,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], @@ -41,6 +42,7 @@ export default class extends Endpoint { private userEntityService: UserEntityService, private userFollowingService: UserFollowingService, private userSuspendService: UserSuspendService, + private roleService: RoleService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { @@ -51,12 +53,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); + if (await this.roleService.isModerator(user)) { + throw new Error('cannot suspend moderator account'); } await this.usersRepository.update(user.id, { diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts deleted file mode 100644 index 3a9d410de0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - await this.usersRepository.update(user.id, { - isSilenced: false, - }); - - this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false }); - - this.moderationLogService.insertModerationLog(me, 'unsilence', { - targetId: user.id, - }); - }); - } -} 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 795b8460f3..c766494e6b 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -19,8 +19,6 @@ export const paramDef = { type: 'object', properties: { disableRegistration: { type: 'boolean', nullable: true }, - disableLocalTimeline: { type: 'boolean', nullable: true }, - disableGlobalTimeline: { type: 'boolean', nullable: true }, useStarForReactionFallback: { type: 'boolean', nullable: true }, pinnedUsers: { type: 'array', nullable: true, items: { type: 'string', @@ -42,7 +40,6 @@ export const paramDef = { description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, - localDriveCapacityMb: { type: 'integer' }, remoteDriveCapacityMb: { type: 'integer' }, cacheRemoteFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, @@ -130,14 +127,6 @@ export default class extends Endpoint { set.disableRegistration = ps.disableRegistration; } - if (typeof ps.disableLocalTimeline === 'boolean') { - set.disableLocalTimeline = ps.disableLocalTimeline; - } - - if (typeof ps.disableGlobalTimeline === 'boolean') { - set.disableGlobalTimeline = ps.disableGlobalTimeline; - } - if (typeof ps.useStarForReactionFallback === 'boolean') { set.useStarForReactionFallback = ps.useStarForReactionFallback; } @@ -194,10 +183,6 @@ export default class extends Endpoint { set.defaultDarkTheme = ps.defaultDarkTheme; } - if (ps.localDriveCapacityMb !== undefined) { - set.localDriveCapacityMb = ps.localDriveCapacityMb; - } - if (ps.remoteDriveCapacityMb !== undefined) { set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 2378660ec8..08625250c8 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -5,6 +5,7 @@ import type { UserListsRepository, UserGroupJoiningsRepository, AntennasReposito import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -83,6 +84,7 @@ export default class extends Endpoint { private userGroupJoiningsRepository: UserGroupJoiningsRepository, private antennaEntityService: AntennaEntityService, + private roleService: RoleService, private idService: IdService, private globalEventService: GlobalEventService, ) { @@ -90,7 +92,7 @@ export default class extends Endpoint { const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); - if (currentAntennasCount > 5) { + if (currentAntennasCount > (await this.roleService.getUserRoleOptions(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 6f40225f15..2a06792dcf 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['drive', 'account'], @@ -38,6 +39,7 @@ export default class extends Endpoint { constructor( private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); @@ -45,8 +47,10 @@ export default class extends Endpoint { // Calculate drive usage const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); + const myRole = await this.roleService.getUserRoleOptions(me.id); + return { - capacity: 1024 * 1024 * (me.driveCapacityOverrideMb ?? instance.localDriveCapacityMb), + capacity: 1024 * 1024 * myRole.driveCapacityMb, usage: usage, }; }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index be7b050907..2ced97ee02 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -46,6 +47,7 @@ export default class extends Endpoint { private driveFilesRepository: DriveFilesRepository, private driveService: DriveService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -55,7 +57,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index 474d599cb6..e0a07a3640 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -62,6 +63,7 @@ export default class extends Endpoint { private driveFilesRepository: DriveFilesRepository, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { let file: DriveFile | null = null; @@ -84,7 +86,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 9e2c767277..0fe57de6a8 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -5,6 +5,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -72,6 +73,7 @@ export default class extends Endpoint { private driveFoldersRepository: DriveFoldersRepository, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -81,7 +83,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index c44d63d64b..f87fca63e3 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -7,6 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -77,18 +78,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - disableLocalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - disableGlobalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - driveCapacityPerLocalUserMb: { - type: 'number', - optional: false, nullable: false, - }, driveCapacityPerRemoteUserMb: { type: 'number', optional: false, nullable: false, @@ -314,9 +303,6 @@ export default class extends Endpoint { repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -353,6 +339,8 @@ export default class extends Endpoint { translatorAvailable: instance.deeplAuthKey != null, + baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, + ...(ps.detail ? { pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, @@ -369,8 +357,6 @@ export default class extends Endpoint { response.proxyAccountName = proxyAccount ? proxyAccount.username : null; response.features = { registration: !instance.disableRegistration, - localTimeLine: !instance.disableLocalTimeline, - globalTimeLine: !instance.disableGlobalTimeline, emailRequiredForSignup: instance.emailRequiredForSignup, elasticsearch: this.config.elasticsearch ? true : false, hcaptcha: instance.enableHcaptcha, diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 3c6e7bf768..16c4c01387 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -4,8 +4,9 @@ import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -51,6 +52,7 @@ export default class extends Endpoint { private usersRepository: UsersRepository, private getterService: GetterService, + private roleService: RoleService, private noteDeleteService: NoteDeleteService, ) { super(meta, paramDef, async (ps, me) => { @@ -59,7 +61,7 @@ export default class extends Endpoint { throw err; }); - if ((!me.isAdmin && !me.isModerator) && (note.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (note.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index b6eaccb5ac..081563493d 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -6,6 +6,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -57,14 +58,13 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableGlobalTimeline) { - if (me == null || (!me.isAdmin && !me.isModerator)) { - throw new ApiError(meta.errors.gtlDisabled); - } + const role = await this.roleService.getUserRoleOptions(me ? me.id : null); + if (!role.gtlAvailable) { + throw new ApiError(meta.errors.gtlDisabled); } //#region Construct query diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 58bbf223a1..b2c504448e 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -7,6 +7,7 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -66,11 +67,12 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableLocalTimeline && (!me.isAdmin && !me.isModerator)) { + const role = await this.roleService.getUserRoleOptions(me.id); + if (!role.ltlAvailable) { throw new ApiError(meta.errors.stlDisabled); } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index d3594814b0..6361edc310 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -7,6 +7,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -62,14 +63,13 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableLocalTimeline) { - if (me == null || (!me.isAdmin && !me.isModerator)) { - throw new ApiError(meta.errors.ltlDisabled); - } + const role = await this.roleService.getUserRoleOptions(me ? me.id : null); + if (!role.ltlAvailable) { + throw new ApiError(meta.errors.ltlDisabled); } //#region Construct query diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index b015129a7a..8becb68a34 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -27,7 +27,7 @@ export const paramDef = { 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', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' }, + state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, hostname: { type: 'string', @@ -54,9 +54,6 @@ export default class extends Endpoint { query.where('user.isExplorable = TRUE'); switch (ps.state) { - case 'admin': query.andWhere('user.isAdmin = TRUE'); break; - case 'moderator': query.andWhere('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; } diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 13badab727..d19d4007d6 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -7,8 +7,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; import { EmailService } from '@/core/EmailService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], @@ -61,6 +62,7 @@ export default class extends Endpoint { private metaService: MetaService, private emailService: EmailService, private getterService: GetterService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -74,7 +76,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.cannotReportYourself); } - if (user.isAdmin) { + if (await this.roleService.isAdministrator(user)) { throw new ApiError(meta.errors.cannotReportAdmin); } @@ -90,13 +92,7 @@ export default class extends Endpoint { // Publish event to moderators setImmediate(async () => { - const moderators = await this.usersRepository.find({ - where: [{ - isAdmin: true, - }, { - isModerator: true, - }], - }); + const moderators = await this.roleService.getModerators(); for (const moderator of moderators) { this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index fcdaeae1c9..70258ef009 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -7,6 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DI } from '@/di-symbols.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import type { FindOptionsWhere } from 'typeorm'; @@ -91,20 +92,21 @@ export default class extends Endpoint { private userEntityService: UserEntityService, private remoteUserResolveService: RemoteUserResolveService, + private roleService: RoleService, private perUserPvChart: PerUserPvChart, private apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { let user; - const isAdminOrModerator = me && (me.isAdmin || me.isModerator); + const isModerator = await this.roleService.isModerator(me); if (ps.userIds) { if (ps.userIds.length === 0) { return []; } - const users = await this.usersRepository.findBy(isAdminOrModerator ? { + const users = await this.usersRepository.findBy(isModerator ? { id: In(ps.userIds), } : { id: In(ps.userIds), @@ -135,7 +137,7 @@ export default class extends Endpoint { user = await this.usersRepository.findOneBy(q); } - if (user == null || (!isAdminOrModerator && user.isSuspended)) { + if (user == null || (!isModerator && user.isSuspended)) { throw new ApiError(meta.errors.noSuchUser); } 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 34f782e580..185c813869 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -7,6 +7,7 @@ import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class GlobalTimelineChannel extends Channel { @@ -16,6 +17,7 @@ class GlobalTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -27,10 +29,8 @@ class GlobalTimelineChannel extends Channel { @bindThis public async init(params: any) { - const meta = await this.metaService.fetch(); - if (meta.disableGlobalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.gtlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -95,6 +95,7 @@ export class GlobalTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -103,6 +104,7 @@ export class GlobalTimelineChannelService { 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/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 6c6afb12bf..a0f75f202c 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class HybridTimelineChannel extends Channel { @@ -17,6 +18,7 @@ class HybridTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -28,8 +30,8 @@ class HybridTimelineChannel extends Channel { @bindThis public async init(params: any): Promise { - const meta = await this.metaService.fetch(); - if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.ltlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -112,6 +114,7 @@ export class HybridTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -120,6 +123,7 @@ export class HybridTimelineChannelService { 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 54388787ef..7d76f42fe7 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -6,6 +6,7 @@ import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class LocalTimelineChannel extends Channel { @@ -15,6 +16,7 @@ class LocalTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -26,10 +28,8 @@ class LocalTimelineChannel extends Channel { @bindThis public async init(params: any) { - const meta = await this.metaService.fetch(); - if (meta.disableLocalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.ltlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -92,6 +92,7 @@ export class LocalTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -100,6 +101,7 @@ export class LocalTimelineChannelService { 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/types.ts b/packages/backend/src/server/api/stream/types.ts index ec05be56ee..3bc844f949 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -14,23 +14,33 @@ import type { Page } from '@/models/entities/Page.js'; import type { Packed } from '@/misc/schema.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import type { Meta } from '@/models/entities/Meta.js'; +import { Role, RoleAssignment } from '@/models'; import type Emitter from 'strict-event-emitter-types'; import type { EventEmitter } from 'events'; +// redis通すとDateのインスタンスはstringに変換されるので +type Serialized = { + [K in keyof T]: T[K] extends Date ? string : T[K]; +}; + //#region Stream type-body definitions export interface InternalStreamTypes { - userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; - userChangeSilencedState: { id: User['id']; isSilenced: User['isSilenced']; }; - userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; }; - userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; }; - remoteUserUpdated: { id: User['id']; }; - webhookCreated: Webhook; - webhookDeleted: Webhook; - webhookUpdated: Webhook; - antennaCreated: Antenna; - antennaDeleted: Antenna; - antennaUpdated: Antenna; - metaUpdated: Meta, + userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>; + userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>; + remoteUserUpdated: Serialized<{ id: User['id']; }>; + defaultRoleOverrideUpdated: Serialized; + roleCreated: Serialized; + roleDeleted: Serialized; + roleUpdated: Serialized; + userRoleAssigned: Serialized; + userRoleUnassigned: Serialized; + webhookCreated: Serialized; + webhookDeleted: Serialized; + webhookUpdated: Serialized; + antennaCreated: Serialized; + antennaDeleted: Serialized; + antennaUpdated: Serialized; + metaUpdated: Serialized; } export interface BroadcastTypes { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 5c29224019..2a764a25b0 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -29,6 +29,7 @@ import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepos import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; import manifest from './manifest.json' assert { type: 'json' }; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; @@ -83,6 +84,7 @@ export class ClientServerService { private metaService: MetaService, private urlPreviewService: UrlPreviewService, private feedService: FeedService, + private roleService: RoleService, @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @@ -125,7 +127,12 @@ export class ClientServerService { throw new Error('login required'); } const user = await this.usersRepository.findOneBy({ token }); - if (user == null || !(user.isAdmin || user.isModerator)) { + if (user == null) { + reply.code(403); + throw new Error('no such user'); + } + const isAdministrator = await this.roleService.isAdministrator(user); + if (!isAdministrator) { reply.code(403); throw new Error('access denied'); } diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 86df3308ec..e2fc27fecd 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -301,6 +301,10 @@ const meta = await res.json(); + if (meta.version == null) { + throw new Error('failed to fetch instance metadata'); + } + if (meta.version != v) { localStorage.setItem('v', meta.version); refresh(); diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue new file mode 100644 index 0000000000..ddd7dbb250 --- /dev/null +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 1a4c494987..be8a4c408e 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -1,5 +1,5 @@ + - - - - - - - - - + + +
@@ -180,12 +181,16 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { iAmAdmin, iAmModerator } from '@/account'; import { instance } from '@/instance'; +import MkRolePreview from '@/components/MkRolePreview.vue'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ userId: string; -}>(); + initialTab?: string; +}>(), { + initialTab: 'overview', +}); -let tab = $ref('overview'); +let tab = $ref(props.initialTab); let chartSrc = $ref('per-user-notes'); let user = $ref(); let init = $ref>(); @@ -195,7 +200,6 @@ let ap = $ref(null); let moderator = $ref(false); let silenced = $ref(false); let suspended = $ref(false); -let driveCapacityOverrideMb: number | null = $ref(0); let moderationNote = $ref(''); const filesPagination = { endpoint: 'admin/drive/files' as const, @@ -220,7 +224,6 @@ function createFetcher() { moderator = info.isModerator; silenced = info.isSilenced; suspended = info.isSuspended; - driveCapacityOverrideMb = user.driveCapacityOverrideMb; moderationNote = info.moderationNote; watch($$(moderationNote), async () => { @@ -257,19 +260,6 @@ async function resetPassword() { }); } -async function toggleSilence(v) { - const confirm = await os.confirm({ - type: 'warning', - text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm, - }); - if (confirm.canceled) { - silenced = !v; - } else { - await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.id }); - await refreshUser(); - } -} - async function toggleSuspend(v) { const confirm = await os.confirm({ type: 'warning', @@ -283,11 +273,6 @@ async function toggleSuspend(v) { } } -async function toggleModerator(v) { - await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: user.id }); - await refreshUser(); -} - async function deleteAllFiles() { const confirm = await os.confirm({ type: 'warning', @@ -307,22 +292,6 @@ async function deleteAllFiles() { await refreshUser(); } -async function applyDriveCapacityOverride() { - let driveCapOrMb = driveCapacityOverrideMb; - if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) { - driveCapOrMb = null; - } - try { - await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb }); - await refreshUser(); - } catch (err) { - os.alert({ - type: 'error', - text: err.toString(), - }); - } -} - async function deleteAccount() { const confirm = await os.confirm({ type: 'warning', @@ -347,6 +316,31 @@ async function deleteAccount() { } } +async function assignRole() { + const roles = await os.api('admin/roles/list'); + + const { canceled, result: roleId } = await os.select({ + title: i18n.ts._role.chooseRoleToAssign, + items: roles.map(r => ({ text: r.name, value: r.id })), + }); + if (canceled) return; + + await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id }); + refreshUser(); +} + +async function unassignRole(role, ev) { + os.popupMenu([{ + text: i18n.ts.unassign, + icon: 'ti ti-x', + danger: true, + action: async () => { + await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id }); + refreshUser(); + }, + }], ev.currentTarget ?? ev.target); +} + watch(() => props.userId, () => { init = createFetcher(); }, { @@ -484,4 +478,19 @@ definePageMetadata(computed(() => ({ margin-left: auto; } } + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnassign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 07d34a794d..eea4d20094 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -18,7 +18,6 @@
-
@@ -35,7 +34,6 @@
-
@@ -189,7 +187,7 @@ onMounted(() => { const bd = parseInt(props.user.birthday.split('-')[2]); if (m === bm && d === bd) { confetti({ - duration: 1000 * 4 + duration: 1000 * 4, }); } } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 4b9f49f8fd..05dcd7806e 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -37,6 +37,7 @@ export const routes = [{ }, { path: '/user-info/:userId', component: page(() => import('./pages/user-info.vue')), + hash: 'initialTab', }, { path: '/instance-info/:host', component: page(() => import('./pages/instance-info.vue')), @@ -351,6 +352,22 @@ export const routes = [{ path: '/ads', name: 'ads', component: page(() => import('./pages/admin/ads.vue')), + }, { + path: '/roles/:id/edit', + name: 'roles', + component: page(() => import('./pages/admin/roles.edit.vue')), + }, { + path: '/roles/new', + name: 'roles', + component: page(() => import('./pages/admin/roles.edit.vue')), + }, { + path: '/roles/:id', + name: 'roles', + component: page(() => import('./pages/admin/roles.role.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('./pages/admin/roles.vue')), }, { path: '/database', name: 'database', diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 7ede64c327..74bd61fd78 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -108,26 +108,6 @@ export function getUserMenu(user, router: Router = mainRouter) { }); } - async function toggleSilence() { - if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; - - os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: user.id, - }).then(() => { - user.isSilenced = !user.isSilenced; - }); - } - - async function toggleSuspend() { - if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; - - os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: user.id, - }).then(() => { - user.isSuspended = !user.isSuspended; - }); - } - function reportAbuse() { os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: user, @@ -218,13 +198,11 @@ export function getUserMenu(user, router: Router = mainRouter) { if (iAmModerator) { menu = menu.concat([null, { - icon: 'ti ti-microphone-2-off', - text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, - action: toggleSilence, - }, { - icon: 'ti ti-snowflake', - text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, - action: toggleSuspend, + icon: 'ti ti-user-exclamation', + text: i18n.ts.moderation, + action: () => { + router.push('/user-info/' + user.id + '#moderation'); + }, }]); } } diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index f75e526939..b8a0a504a3 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -45,9 +45,7 @@ onMounted(() => { if (props.column.tl == null) { setType(); } else if ($i) { - disabled = !$i.isModerator && !$i.isAdmin && ( - instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) || - instance.disableGlobalTimeline && ['global'].includes(props.column.tl)); + disabled = false; // TODO } }); -- cgit v1.2.3-freya