From 5728350267faa95e171840b7573d767617ce5491 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 25 Jun 2022 07:23:59 +0200 Subject: fix: always respect instance mutes (#8854) * fix: muted user query also checks instances This way it can be ensured that the instance mute is used everywhere it is required without checking the whole codebase again. Muted users and muted instances should be used together anyways. * fix lint --- packages/backend/src/server/api/endpoints/users/notes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/users/notes.ts') diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index aec5c0ea99..9fa56fe83a 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -7,7 +7,6 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; export const meta = { tags: ['users', 'notes'], @@ -77,9 +76,10 @@ export default define(meta, paramDef, async (ps, me) => { .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me, user); - if (me) generateBlockedUserQuery(query, me); - if (me) generateMutedInstanceQuery(query, me); + if (me) { + generateMutedUserQuery(query, me, user); + generateBlockedUserQuery(query, me); + } if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); -- cgit v1.2.3-freya 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 --- .github/workflows/test.yml | 8 +- CONTRIBUTING.md | 2 +- package.json | 6 +- packages/backend/.mocharc.json | 10 - packages/backend/jest-resolver.cjs | 14 + packages/backend/jest.config.cjs | 214 ++ packages/backend/ormconfig.js | 6 +- packages/backend/package.json | 17 +- packages/backend/src/AppModule.ts | 15 + packages/backend/src/GlobalModule.ts | 63 + packages/backend/src/RepositoryModule.ts | 519 +++++ packages/backend/src/boot/index.ts | 2 +- packages/backend/src/boot/master.ts | 37 +- packages/backend/src/boot/worker.ts | 20 +- packages/backend/src/config.ts | 149 ++ packages/backend/src/config/index.ts | 3 - packages/backend/src/config/load.ts | 62 - packages/backend/src/config/types.ts | 87 - packages/backend/src/core/AccountUpdateService.ts | 38 + packages/backend/src/core/AiService.ts | 60 + packages/backend/src/core/AntennaService.ts | 228 +++ packages/backend/src/core/AppLockService.ts | 40 + packages/backend/src/core/CaptchaService.ts | 70 + packages/backend/src/core/CoreModule.ts | 696 +++++++ .../backend/src/core/CreateNotificationService.ts | 114 ++ .../backend/src/core/CreateSystemUserService.ts | 80 + packages/backend/src/core/CustomEmojiService.ts | 175 ++ packages/backend/src/core/DeleteAccountService.ts | 38 + packages/backend/src/core/DownloadService.ts | 123 ++ packages/backend/src/core/DriveService.ts | 740 +++++++ packages/backend/src/core/EmailService.ts | 175 ++ .../backend/src/core/FederatedInstanceService.ts | 46 + .../src/core/FetchInstanceMetadataService.ts | 283 +++ packages/backend/src/core/FileInfoService.ts | 382 ++++ packages/backend/src/core/GlobalEventService.ts | 109 + packages/backend/src/core/HashtagService.ts | 147 ++ packages/backend/src/core/HttpRequestService.ts | 154 ++ packages/backend/src/core/IdService.ts | 33 + .../backend/src/core/ImageProcessingService.ts | 99 + packages/backend/src/core/InstanceActorService.ts | 42 + .../backend/src/core/InternalStorageService.ts | 45 + packages/backend/src/core/MessagingService.ts | 300 +++ packages/backend/src/core/MetaService.ts | 63 + packages/backend/src/core/MfmService.ts | 384 ++++ packages/backend/src/core/ModerationLogService.ts | 26 + packages/backend/src/core/NoteCreateService.ts | 742 +++++++ packages/backend/src/core/NoteDeleteService.ts | 168 ++ packages/backend/src/core/NotePiningService.ts | 117 ++ packages/backend/src/core/NoteReadService.ts | 214 ++ packages/backend/src/core/NotificationService.ts | 67 + packages/backend/src/core/PollService.ts | 115 ++ packages/backend/src/core/ProxyAccountService.ts | 22 + .../backend/src/core/PushNotificationService.ts | 101 + packages/backend/src/core/QueryService.ts | 262 +++ packages/backend/src/core/QueueService.ts | 242 +++ packages/backend/src/core/ReactionService.ts | 340 ++++ packages/backend/src/core/RelayService.ts | 119 ++ packages/backend/src/core/S3Service.ts | 38 + packages/backend/src/core/SignupService.ts | 141 ++ .../src/core/TwoFactorAuthenticationService.ts | 439 ++++ packages/backend/src/core/UserBlockingService.ts | 199 ++ packages/backend/src/core/UserCacheService.ts | 74 + packages/backend/src/core/UserFollowingService.ts | 574 ++++++ .../backend/src/core/UserKeypairStoreService.ts | 22 + packages/backend/src/core/UserListService.ts | 48 + packages/backend/src/core/UserMutingService.ts | 32 + packages/backend/src/core/UserSuspendService.ts | 88 + packages/backend/src/core/UtilityService.ts | 37 + .../backend/src/core/VideoProcessingService.ts | 44 + packages/backend/src/core/WebhookService.ts | 70 + .../src/core/chart/ChartManagementService.ts | 59 + .../backend/src/core/chart/charts/active-users.ts | 54 + .../backend/src/core/chart/charts/ap-request.ts | 49 + packages/backend/src/core/chart/charts/drive.ts | 47 + .../src/core/chart/charts/entities/active-users.ts | 17 + .../src/core/chart/charts/entities/ap-request.ts | 11 + .../src/core/chart/charts/entities/drive.ts | 16 + .../src/core/chart/charts/entities/federation.ts | 16 + .../src/core/chart/charts/entities/hashtag.ts | 10 + .../src/core/chart/charts/entities/instance.ts | 32 + .../src/core/chart/charts/entities/notes.ts | 22 + .../core/chart/charts/entities/per-user-drive.ts | 14 + .../chart/charts/entities/per-user-following.ts | 20 + .../core/chart/charts/entities/per-user-notes.ts | 15 + .../chart/charts/entities/per-user-reactions.ts | 10 + .../src/core/chart/charts/entities/test-grouped.ts | 11 + .../chart/charts/entities/test-intersection.ts | 11 + .../src/core/chart/charts/entities/test-unique.ts | 9 + .../backend/src/core/chart/charts/entities/test.ts | 11 + .../src/core/chart/charts/entities/users.ts | 14 + .../backend/src/core/chart/charts/federation.ts | 121 ++ packages/backend/src/core/chart/charts/hashtag.ts | 41 + packages/backend/src/core/chart/charts/instance.ts | 127 ++ packages/backend/src/core/chart/charts/notes.ts | 58 + .../src/core/chart/charts/per-user-drive.ts | 58 + .../src/core/chart/charts/per-user-following.ts | 71 + .../src/core/chart/charts/per-user-notes.ts | 55 + .../src/core/chart/charts/per-user-reactions.ts | 42 + .../backend/src/core/chart/charts/test-grouped.ts | 46 + .../src/core/chart/charts/test-intersection.ts | 43 + .../backend/src/core/chart/charts/test-unique.ts | 37 + packages/backend/src/core/chart/charts/test.ts | 53 + packages/backend/src/core/chart/charts/users.ts | 56 + packages/backend/src/core/chart/core.ts | 679 +++++++ packages/backend/src/core/chart/entities.ts | 39 + .../core/entities/AbuseUserReportEntityService.ts | 49 + .../src/core/entities/AntennaEntityService.ts | 47 + .../backend/src/core/entities/AppEntityService.ts | 52 + .../src/core/entities/AuthSessionEntityService.ts | 33 + .../src/core/entities/BlockingEntityService.ts | 42 + .../src/core/entities/ChannelEntityService.ts | 66 + .../backend/src/core/entities/ClipEntityService.ts | 43 + .../src/core/entities/DriveFileEntityService.ts | 214 ++ .../src/core/entities/DriveFolderEntityService.ts | 57 + .../src/core/entities/EmojiEntityService.ts | 43 + .../core/entities/FollowRequestEntityService.ts | 34 + .../src/core/entities/FollowingEntityService.ts | 98 + .../src/core/entities/GalleryLikeEntityService.ts | 41 + .../src/core/entities/GalleryPostEntityService.ts | 57 + .../src/core/entities/HashtagEntityService.ts | 41 + .../src/core/entities/InstanceEntityService.ts | 59 + .../core/entities/MessagingMessageEntityService.ts | 57 + .../core/entities/ModerationLogEntityService.ts | 44 + .../src/core/entities/MutingEntityService.ts | 45 + .../backend/src/core/entities/NoteEntityService.ts | 396 ++++ .../src/core/entities/NoteFavoriteEntityService.ts | 42 + .../src/core/entities/NoteReactionEntityService.ts | 62 + .../src/core/entities/NotificationEntityService.ts | 151 ++ .../backend/src/core/entities/PageEntityService.ts | 109 + .../src/core/entities/PageLikeEntityService.ts | 41 + .../src/core/entities/SigninEntityService.ts | 27 + .../backend/src/core/entities/UserEntityService.ts | 515 +++++ .../src/core/entities/UserGroupEntityService.ts | 42 + .../entities/UserGroupInvitationEntityService.ts | 39 + .../src/core/entities/UserListEntityService.ts | 41 + packages/backend/src/core/queue/QueueModule.ts | 112 ++ .../backend/src/core/remote/RemoteLoggerService.ts | 12 + .../backend/src/core/remote/ResolveUserService.ts | 132 ++ .../backend/src/core/remote/WebfingerService.ts | 48 + .../core/remote/activitypub/ApAudienceService.ts | 104 + .../core/remote/activitypub/ApDbResolverService.ts | 179 ++ .../remote/activitypub/ApDeliverManagerService.ts | 199 ++ .../src/core/remote/activitypub/ApInboxService.ts | 735 +++++++ .../src/core/remote/activitypub/ApLoggerService.ts | 14 + .../src/core/remote/activitypub/ApMfmService.ts | 30 + .../core/remote/activitypub/ApRendererService.ts | 702 +++++++ .../core/remote/activitypub/ApRequestService.ts | 182 ++ .../core/remote/activitypub/ApResolverService.ts | 190 ++ .../core/remote/activitypub/LdSignatureService.ts | 150 ++ .../src/core/remote/activitypub/misc/contexts.ts | 526 +++++ .../core/remote/activitypub/misc/get-note-html.ts | 8 + .../remote/activitypub/models/ApImageService.ts | 90 + .../remote/activitypub/models/ApMentionService.ts | 41 + .../remote/activitypub/models/ApNoteService.ts | 403 ++++ .../remote/activitypub/models/ApPersonService.ts | 594 ++++++ .../remote/activitypub/models/ApQuestionService.ts | 109 + .../src/core/remote/activitypub/models/icon.ts | 5 + .../core/remote/activitypub/models/identifier.ts | 5 + .../src/core/remote/activitypub/models/tag.ts | 19 + .../backend/src/core/remote/activitypub/type.ts | 295 +++ packages/backend/src/daemons/DaemonModule.ts | 24 + packages/backend/src/daemons/JanitorService.ts | 37 + packages/backend/src/daemons/QueueStatsService.ts | 77 + packages/backend/src/daemons/ServerStatsService.ts | 95 + packages/backend/src/daemons/janitor.ts | 20 - packages/backend/src/daemons/queue-stats.ts | 60 - packages/backend/src/daemons/server-stats.ts | 79 - packages/backend/src/db/elasticsearch.ts | 56 - packages/backend/src/db/logger.ts | 3 - packages/backend/src/db/postgre.ts | 256 --- packages/backend/src/db/redis.ts | 18 - packages/backend/src/di-symbols.ts | 72 + packages/backend/src/logger.ts | 112 ++ packages/backend/src/mfm/from-html.ts | 213 -- packages/backend/src/mfm/to-html.ts | 159 -- packages/backend/src/misc/acct.ts | 2 +- packages/backend/src/misc/antenna-cache.ts | 36 - packages/backend/src/misc/app-lock.ts | 31 - packages/backend/src/misc/before-shutdown.ts | 2 +- packages/backend/src/misc/captcha.ts | 57 - packages/backend/src/misc/check-hit-antenna.ts | 99 - packages/backend/src/misc/check-word-mute.ts | 4 +- packages/backend/src/misc/convert-host.ts | 26 - packages/backend/src/misc/count-same-renotes.ts | 15 - packages/backend/src/misc/detect-url-mime.ts | 15 - packages/backend/src/misc/download-text-file.ts | 25 - packages/backend/src/misc/download-url.ts | 89 - .../src/misc/extract-custom-emojis-from-mfm.ts | 2 +- packages/backend/src/misc/extract-hashtags.ts | 2 +- packages/backend/src/misc/extract-mentions.ts | 2 +- packages/backend/src/misc/fetch-meta.ts | 44 - packages/backend/src/misc/fetch-proxy-account.ts | 9 - packages/backend/src/misc/fetch.ts | 141 -- packages/backend/src/misc/gen-id.ts | 21 - .../backend/src/misc/generate-native-user-token.ts | 3 + packages/backend/src/misc/get-file-info.ts | 374 ---- packages/backend/src/misc/get-note-summary.ts | 10 +- packages/backend/src/misc/identifiable-error.ts | 2 +- packages/backend/src/misc/is-native-token.ts | 1 + packages/backend/src/misc/is-quote.ts | 2 +- packages/backend/src/misc/keypair-store.ts | 10 - packages/backend/src/misc/populate-emojis.ts | 125 -- packages/backend/src/misc/prelude/README.md | 3 + packages/backend/src/misc/prelude/array.ts | 138 ++ packages/backend/src/misc/prelude/await-all.ts | 21 + packages/backend/src/misc/prelude/math.ts | 3 + packages/backend/src/misc/prelude/maybe.ts | 20 + packages/backend/src/misc/prelude/relation.ts | 5 + packages/backend/src/misc/prelude/string.ts | 15 + packages/backend/src/misc/prelude/symbol.ts | 1 + packages/backend/src/misc/prelude/time.ts | 39 + packages/backend/src/misc/prelude/url.ts | 13 + packages/backend/src/misc/prelude/xml.ts | 41 + packages/backend/src/misc/reaction-lib.ts | 131 -- packages/backend/src/misc/reset-db.ts | 28 + packages/backend/src/misc/show-machine-info.ts | 2 +- packages/backend/src/misc/status-error.ts | 13 + packages/backend/src/misc/webhook-cache.ts | 49 - .../backend/src/models/entities/AbuseUserReport.ts | 79 + .../backend/src/models/entities/AccessToken.ts | 90 + packages/backend/src/models/entities/Ad.ts | 59 + .../backend/src/models/entities/Announcement.ts | 43 + .../src/models/entities/AnnouncementRead.ts | 36 + packages/backend/src/models/entities/Antenna.ts | 99 + .../backend/src/models/entities/AntennaNote.ts | 43 + packages/backend/src/models/entities/App.ts | 60 + .../src/models/entities/AttestationChallenge.ts | 46 + .../backend/src/models/entities/AuthSession.ts | 43 + packages/backend/src/models/entities/Blocking.ts | 42 + packages/backend/src/models/entities/Channel.ts | 75 + .../src/models/entities/ChannelFollowing.ts | 43 + .../src/models/entities/ChannelNotePining.ts | 35 + packages/backend/src/models/entities/Clip.ts | 44 + packages/backend/src/models/entities/ClipNote.ts | 37 + packages/backend/src/models/entities/DriveFile.ts | 192 ++ .../backend/src/models/entities/DriveFolder.ts | 49 + packages/backend/src/models/entities/Emoji.ts | 58 + .../backend/src/models/entities/FollowRequest.ts | 85 + packages/backend/src/models/entities/Following.ts | 82 + .../backend/src/models/entities/GalleryLike.ts | 33 + .../backend/src/models/entities/GalleryPost.ts | 79 + packages/backend/src/models/entities/Hashtag.ts | 87 + packages/backend/src/models/entities/Instance.ts | 164 ++ .../src/models/entities/MessagingMessage.ts | 89 + packages/backend/src/models/entities/Meta.ts | 462 +++++ .../backend/src/models/entities/ModerationLog.ts | 32 + packages/backend/src/models/entities/MutedNote.ts | 48 + packages/backend/src/models/entities/Muting.ts | 48 + packages/backend/src/models/entities/Note.ts | 242 +++ .../backend/src/models/entities/NoteFavorite.ts | 35 + .../backend/src/models/entities/NoteReaction.ts | 44 + .../src/models/entities/NoteThreadMuting.ts | 33 + packages/backend/src/models/entities/NoteUnread.ts | 63 + .../backend/src/models/entities/Notification.ts | 173 ++ packages/backend/src/models/entities/Page.ts | 121 ++ packages/backend/src/models/entities/PageLike.ts | 33 + .../src/models/entities/PasswordResetRequest.ts | 30 + packages/backend/src/models/entities/Poll.ts | 72 + packages/backend/src/models/entities/PollVote.ts | 40 + packages/backend/src/models/entities/PromoNote.ts | 28 + packages/backend/src/models/entities/PromoRead.ts | 35 + .../src/models/entities/RegistrationTickets.ts | 17 + .../backend/src/models/entities/RegistryItem.ts | 58 + packages/backend/src/models/entities/Relay.ts | 19 + packages/backend/src/models/entities/Signin.ts | 35 + .../backend/src/models/entities/SwSubscription.ts | 37 + .../backend/src/models/entities/UsedUsername.ts | 20 + packages/backend/src/models/entities/User.ts | 255 +++ packages/backend/src/models/entities/UserGroup.ts | 46 + .../src/models/entities/UserGroupInvitation.ts | 42 + .../src/models/entities/UserGroupJoining.ts | 42 + packages/backend/src/models/entities/UserIp.ts | 24 + .../backend/src/models/entities/UserKeypair.ts | 33 + packages/backend/src/models/entities/UserList.ts | 33 + .../backend/src/models/entities/UserListJoining.ts | 42 + .../backend/src/models/entities/UserNotePining.ts | 35 + .../backend/src/models/entities/UserPending.ts | 32 + .../backend/src/models/entities/UserProfile.ts | 232 +++ .../backend/src/models/entities/UserPublickey.ts | 34 + .../backend/src/models/entities/UserSecurityKey.ts | 48 + packages/backend/src/models/entities/Webhook.ts | 73 + .../src/models/entities/abuse-user-report.ts | 79 - .../backend/src/models/entities/access-token.ts | 90 - packages/backend/src/models/entities/ad.ts | 59 - .../src/models/entities/announcement-read.ts | 36 - .../backend/src/models/entities/announcement.ts | 43 - .../backend/src/models/entities/antenna-note.ts | 43 - packages/backend/src/models/entities/antenna.ts | 99 - packages/backend/src/models/entities/app.ts | 60 - .../src/models/entities/attestation-challenge.ts | 46 - .../backend/src/models/entities/auth-session.ts | 43 - packages/backend/src/models/entities/blocking.ts | 42 - .../src/models/entities/channel-following.ts | 43 - .../src/models/entities/channel-note-pining.ts | 35 - packages/backend/src/models/entities/channel.ts | 75 - packages/backend/src/models/entities/clip-note.ts | 37 - packages/backend/src/models/entities/clip.ts | 44 - packages/backend/src/models/entities/drive-file.ts | 192 -- .../backend/src/models/entities/drive-folder.ts | 49 - packages/backend/src/models/entities/emoji.ts | 58 - .../backend/src/models/entities/follow-request.ts | 85 - packages/backend/src/models/entities/following.ts | 82 - .../backend/src/models/entities/gallery-like.ts | 33 - .../backend/src/models/entities/gallery-post.ts | 79 - packages/backend/src/models/entities/hashtag.ts | 87 - packages/backend/src/models/entities/instance.ts | 164 -- .../src/models/entities/messaging-message.ts | 89 - packages/backend/src/models/entities/meta.ts | 462 ----- .../backend/src/models/entities/moderation-log.ts | 32 - packages/backend/src/models/entities/muted-note.ts | 48 - packages/backend/src/models/entities/muting.ts | 48 - .../backend/src/models/entities/note-favorite.ts | 35 - .../backend/src/models/entities/note-reaction.ts | 44 - .../src/models/entities/note-thread-muting.ts | 33 - .../backend/src/models/entities/note-unread.ts | 63 - .../backend/src/models/entities/note-watching.ts | 52 - packages/backend/src/models/entities/note.ts | 242 --- .../backend/src/models/entities/notification.ts | 173 -- packages/backend/src/models/entities/page-like.ts | 33 - packages/backend/src/models/entities/page.ts | 121 -- .../src/models/entities/password-reset-request.ts | 30 - packages/backend/src/models/entities/poll-vote.ts | 40 - packages/backend/src/models/entities/poll.ts | 72 - packages/backend/src/models/entities/promo-note.ts | 28 - packages/backend/src/models/entities/promo-read.ts | 35 - .../src/models/entities/registration-tickets.ts | 17 - .../backend/src/models/entities/registry-item.ts | 58 - packages/backend/src/models/entities/relay.ts | 19 - packages/backend/src/models/entities/signin.ts | 35 - .../backend/src/models/entities/sw-subscription.ts | 37 - .../backend/src/models/entities/used-username.ts | 20 - .../src/models/entities/user-group-invitation.ts | 42 - .../src/models/entities/user-group-joining.ts | 42 - packages/backend/src/models/entities/user-group.ts | 46 - packages/backend/src/models/entities/user-ip.ts | 24 - .../backend/src/models/entities/user-keypair.ts | 33 - .../src/models/entities/user-list-joining.ts | 42 - packages/backend/src/models/entities/user-list.ts | 33 - .../src/models/entities/user-note-pining.ts | 35 - .../backend/src/models/entities/user-pending.ts | 32 - .../backend/src/models/entities/user-profile.ts | 232 --- .../backend/src/models/entities/user-publickey.ts | 34 - .../src/models/entities/user-security-key.ts | 48 - packages/backend/src/models/entities/user.ts | 248 --- packages/backend/src/models/entities/webhook.ts | 73 - packages/backend/src/models/index.ts | 323 +-- .../src/models/repositories/abuse-user-report.ts | 38 - .../backend/src/models/repositories/antenna.ts | 32 - packages/backend/src/models/repositories/app.ts | 39 - .../src/models/repositories/auth-session.ts | 20 - .../backend/src/models/repositories/blocking.ts | 31 - .../backend/src/models/repositories/channel.ts | 41 - packages/backend/src/models/repositories/clip.ts | 30 - .../backend/src/models/repositories/drive-file.ts | 188 -- .../src/models/repositories/drive-folder.ts | 42 - packages/backend/src/models/repositories/emoji.ts | 27 - .../src/models/repositories/follow-request.ts | 19 - .../backend/src/models/repositories/following.ts | 85 - .../src/models/repositories/gallery-like.ts | 24 - .../src/models/repositories/gallery-post.ts | 39 - .../backend/src/models/repositories/hashtag.ts | 25 - .../backend/src/models/repositories/instance.ts | 43 - .../src/models/repositories/messaging-message.ts | 39 - .../src/models/repositories/moderation-logs.ts | 29 - packages/backend/src/models/repositories/muting.ts | 32 - .../src/models/repositories/note-favorite.ts | 27 - .../src/models/repositories/note-reaction.ts | 32 - packages/backend/src/models/repositories/note.ts | 326 --- .../src/models/repositories/notification.ts | 115 -- .../backend/src/models/repositories/page-like.ts | 25 - packages/backend/src/models/repositories/page.ts | 88 - packages/backend/src/models/repositories/relay.ts | 5 - packages/backend/src/models/repositories/signin.ts | 10 - .../models/repositories/user-group-invitation.ts | 22 - .../backend/src/models/repositories/user-group.ts | 24 - .../backend/src/models/repositories/user-list.ts | 23 - packages/backend/src/models/repositories/user.ts | 436 ---- .../src/models/schema/federation-instance.ts | 3 - packages/backend/src/postgre.ts | 215 ++ packages/backend/src/prelude/README.md | 3 - packages/backend/src/prelude/array.ts | 138 -- packages/backend/src/prelude/await-all.ts | 21 - packages/backend/src/prelude/math.ts | 3 - packages/backend/src/prelude/maybe.ts | 20 - packages/backend/src/prelude/relation.ts | 5 - packages/backend/src/prelude/string.ts | 15 - packages/backend/src/prelude/symbol.ts | 1 - packages/backend/src/prelude/time.ts | 39 - packages/backend/src/prelude/url.ts | 13 - packages/backend/src/prelude/xml.ts | 41 - .../backend/src/queue/DbQueueProcessorsService.ts | 63 + .../queue/ObjectStorageQueueProcessorsService.ts | 30 + packages/backend/src/queue/QueueLoggerService.ts | 12 + packages/backend/src/queue/QueueProcessorModule.ts | 72 + .../backend/src/queue/QueueProcessorService.ts | 141 ++ .../src/queue/SystemQueueProcessorsService.ts | 38 + packages/backend/src/queue/index.ts | 342 ---- packages/backend/src/queue/initialize.ts | 34 - packages/backend/src/queue/logger.ts | 3 - .../CheckExpiredMutingsProcessorService.ts | 50 + .../processors/CleanChartsProcessorService.ts | 68 + .../src/queue/processors/CleanProcessorService.ts | 36 + .../processors/CleanRemoteFilesProcessorService.ts | 69 + .../processors/DeleteAccountProcessorService.ts | 124 ++ .../processors/DeleteDriveFilesProcessorService.ts | 78 + .../queue/processors/DeleteFileProcessorService.ts | 31 + .../queue/processors/DeliverProcessorService.ts | 130 ++ .../EndedPollNotificationProcessorService.ts | 56 + .../processors/ExportBlockingProcessorService.ts | 117 ++ .../ExportCustomEmojisProcessorService.ts | 135 ++ .../processors/ExportFollowingProcessorService.ts | 120 ++ .../processors/ExportMutingProcessorService.ts | 120 ++ .../processors/ExportNotesProcessorService.ts | 143 ++ .../processors/ExportUserListsProcessorService.ts | 96 + .../processors/ImportBlockingProcessorService.ts | 102 + .../ImportCustomEmojisProcessorService.ts | 110 + .../processors/ImportFollowingProcessorService.ts | 99 + .../processors/ImportMutingProcessorService.ts | 100 + .../processors/ImportUserListsProcessorService.ts | 112 ++ .../src/queue/processors/InboxProcessorService.ts | 195 ++ .../processors/ResyncChartsProcessorService.ts | 61 + .../queue/processors/TickChartsProcessorService.ts | 68 + .../processors/WebhookDeliverProcessorService.ts | 79 + .../src/queue/processors/db/delete-account.ts | 94 - .../src/queue/processors/db/delete-drive-files.ts | 56 - .../src/queue/processors/db/export-blocking.ts | 93 - .../queue/processors/db/export-custom-emojis.ts | 114 -- .../src/queue/processors/db/export-following.ts | 94 - .../backend/src/queue/processors/db/export-mute.ts | 94 - .../src/queue/processors/db/export-notes.ts | 118 -- .../src/queue/processors/db/export-user-lists.ts | 70 - .../src/queue/processors/db/import-blocking.ts | 75 - .../queue/processors/db/import-custom-emojis.ts | 81 - .../src/queue/processors/db/import-following.ts | 74 - .../src/queue/processors/db/import-muting.ts | 84 - .../src/queue/processors/db/import-user-lists.ts | 80 - packages/backend/src/queue/processors/db/index.ts | 37 - packages/backend/src/queue/processors/deliver.ts | 98 - .../queue/processors/ended-poll-notification.ts | 33 - packages/backend/src/queue/processors/inbox.ts | 157 -- .../object-storage/clean-remote-files.ts | 50 - .../queue/processors/object-storage/delete-file.ts | 11 - .../src/queue/processors/object-storage/index.ts | 15 - .../processors/system/check-expired-mutings.ts | 30 - .../src/queue/processors/system/clean-charts.ts | 28 - .../backend/src/queue/processors/system/clean.ts | 18 - .../backend/src/queue/processors/system/index.ts | 20 - .../src/queue/processors/system/resync-charts.ts | 21 - .../src/queue/processors/system/tick-charts.ts | 28 - .../src/queue/processors/webhook-deliver.ts | 59 - packages/backend/src/queue/queues.ts | 21 - packages/backend/src/queue/types.ts | 12 +- packages/backend/src/redis.ts | 15 + .../backend/src/remote/activitypub/ap-request.ts | 104 - .../backend/src/remote/activitypub/audience.ts | 92 - .../backend/src/remote/activitypub/db-resolver.ts | 155 -- .../src/remote/activitypub/deliver-manager.ts | 151 -- .../src/remote/activitypub/kernel/accept/follow.ts | 29 - .../src/remote/activitypub/kernel/accept/index.ts | 24 - .../src/remote/activitypub/kernel/add/index.ts | 23 - .../remote/activitypub/kernel/announce/index.ts | 19 - .../src/remote/activitypub/kernel/announce/note.ts | 72 - .../src/remote/activitypub/kernel/block/index.ts | 23 - .../src/remote/activitypub/kernel/create/index.ts | 43 - .../src/remote/activitypub/kernel/create/note.ts | 44 - .../src/remote/activitypub/kernel/delete/actor.ts | 27 - .../src/remote/activitypub/kernel/delete/index.ts | 49 - .../src/remote/activitypub/kernel/delete/note.ts | 41 - .../src/remote/activitypub/kernel/flag/index.ts | 30 - .../src/remote/activitypub/kernel/follow.ts | 20 - .../backend/src/remote/activitypub/kernel/index.ts | 74 - .../backend/src/remote/activitypub/kernel/like.ts | 21 - .../backend/src/remote/activitypub/kernel/read.ts | 27 - .../src/remote/activitypub/kernel/reject/follow.ts | 30 - .../src/remote/activitypub/kernel/reject/index.ts | 24 - .../src/remote/activitypub/kernel/remove/index.ts | 23 - .../src/remote/activitypub/kernel/undo/accept.ts | 27 - .../src/remote/activitypub/kernel/undo/announce.ts | 18 - .../src/remote/activitypub/kernel/undo/block.ts | 21 - .../src/remote/activitypub/kernel/undo/follow.ts | 41 - .../src/remote/activitypub/kernel/undo/index.ts | 36 - .../src/remote/activitypub/kernel/undo/like.ts | 21 - .../src/remote/activitypub/kernel/update/index.ts | 34 - packages/backend/src/remote/activitypub/logger.ts | 3 - .../src/remote/activitypub/misc/contexts.ts | 526 ----- .../src/remote/activitypub/misc/get-note-html.ts | 8 - .../src/remote/activitypub/misc/html-to-mfm.ts | 9 - .../src/remote/activitypub/misc/ld-signature.ts | 135 -- .../backend/src/remote/activitypub/models/icon.ts | 5 - .../src/remote/activitypub/models/identifier.ts | 5 - .../backend/src/remote/activitypub/models/image.ts | 68 - .../src/remote/activitypub/models/mention.ts | 24 - .../backend/src/remote/activitypub/models/note.ts | 359 ---- .../src/remote/activitypub/models/person.ts | 504 ----- .../src/remote/activitypub/models/question.ts | 83 - .../backend/src/remote/activitypub/models/tag.ts | 18 - packages/backend/src/remote/activitypub/perform.ts | 17 - .../src/remote/activitypub/renderer/accept.ts | 8 - .../backend/src/remote/activitypub/renderer/add.ts | 9 - .../src/remote/activitypub/renderer/announce.ts | 29 - .../src/remote/activitypub/renderer/block.ts | 20 - .../src/remote/activitypub/renderer/create.ts | 17 - .../src/remote/activitypub/renderer/delete.ts | 9 - .../src/remote/activitypub/renderer/document.ts | 9 - .../src/remote/activitypub/renderer/emoji.ts | 14 - .../src/remote/activitypub/renderer/flag.ts | 15 - .../remote/activitypub/renderer/follow-relay.ts | 14 - .../src/remote/activitypub/renderer/follow-user.ts | 12 - .../src/remote/activitypub/renderer/follow.ts | 14 - .../src/remote/activitypub/renderer/hashtag.ts | 7 - .../src/remote/activitypub/renderer/image.ts | 9 - .../src/remote/activitypub/renderer/index.ts | 59 - .../backend/src/remote/activitypub/renderer/key.ts | 14 - .../src/remote/activitypub/renderer/like.ts | 31 - .../src/remote/activitypub/renderer/mention.ts | 9 - .../src/remote/activitypub/renderer/note.ts | 169 -- .../renderer/ordered-collection-page.ts | 23 - .../activitypub/renderer/ordered-collection.ts | 28 - .../src/remote/activitypub/renderer/person.ts | 89 - .../src/remote/activitypub/renderer/question.ts | 23 - .../src/remote/activitypub/renderer/read.ts | 9 - .../src/remote/activitypub/renderer/reject.ts | 8 - .../src/remote/activitypub/renderer/remove.ts | 9 - .../src/remote/activitypub/renderer/tombstone.ts | 4 - .../src/remote/activitypub/renderer/undo.ts | 15 - .../src/remote/activitypub/renderer/update.ts | 15 - .../src/remote/activitypub/renderer/vote.ts | 23 - packages/backend/src/remote/activitypub/request.ts | 58 - .../backend/src/remote/activitypub/resolver.ts | 133 -- packages/backend/src/remote/activitypub/type.ts | 295 --- packages/backend/src/remote/logger.ts | 3 - packages/backend/src/remote/resolve-user.ts | 111 -- packages/backend/src/remote/webfinger.ts | 34 - .../backend/src/server/ActivityPubServerService.ts | 584 ++++++ packages/backend/src/server/FileServerService.ts | 178 ++ .../backend/src/server/MediaProxyServerService.ts | 137 ++ .../backend/src/server/NodeinfoServerService.ts | 129 ++ packages/backend/src/server/ServerModule.ts | 92 + packages/backend/src/server/ServerService.ts | 177 ++ .../backend/src/server/WellKnownServerService.ts | 168 ++ packages/backend/src/server/activitypub.ts | 254 --- .../backend/src/server/activitypub/featured.ts | 41 - .../backend/src/server/activitypub/followers.ts | 95 - .../backend/src/server/activitypub/following.ts | 95 - packages/backend/src/server/activitypub/outbox.ts | 108 - packages/backend/src/server/api/2fa.ts | 422 ---- packages/backend/src/server/api/ApiCallService.ts | 258 +++ .../backend/src/server/api/ApiLoggerService.ts | 12 + .../backend/src/server/api/ApiServerService.ts | 160 ++ .../backend/src/server/api/AuthenticateService.ts | 86 + packages/backend/src/server/api/EndpointsModule.ts | 1268 ++++++++++++ .../backend/src/server/api/RateLimiterService.ts | 89 + .../backend/src/server/api/SigninApiService.ts | 282 +++ packages/backend/src/server/api/SigninService.ts | 64 + .../backend/src/server/api/SignupApiService.ts | 175 ++ .../src/server/api/StreamingApiServerService.ts | 120 ++ packages/backend/src/server/api/api-handler.ts | 92 - packages/backend/src/server/api/authenticate.ts | 66 - packages/backend/src/server/api/call.ts | 147 -- .../backend/src/server/api/common/GetterService.ts | 71 + .../src/server/api/common/generate-block-query.ts | 42 - .../server/api/common/generate-channel-query.ts | 24 - .../server/api/common/generate-muted-note-query.ts | 13 - .../api/common/generate-muted-note-thread-query.ts | 17 - .../server/api/common/generate-muted-user-query.ts | 57 - .../api/common/generate-native-user-token.ts | 3 - .../server/api/common/generate-replies-query.ts | 27 - .../server/api/common/generate-visibility-query.ts | 42 - packages/backend/src/server/api/common/getters.ts | 56 - .../src/server/api/common/inject-featured.ts | 4 +- .../backend/src/server/api/common/inject-promo.ts | 4 +- .../src/server/api/common/is-native-token.ts | 1 - .../src/server/api/common/make-pagination-query.ts | 28 - .../server/api/common/read-messaging-message.ts | 151 -- .../src/server/api/common/read-notification.ts | 50 - packages/backend/src/server/api/common/signin.ts | 44 - packages/backend/src/server/api/common/signup.ts | 114 -- packages/backend/src/server/api/define.ts | 59 - packages/backend/src/server/api/endpoint-base.ts | 62 + packages/backend/src/server/api/endpoints.ts | 10 +- .../api/endpoints/admin/abuse-user-reports.ts | 56 +- .../server/api/endpoints/admin/accounts/create.ts | 67 +- .../server/api/endpoints/admin/accounts/delete.ts | 82 +- .../src/server/api/endpoints/admin/ad/create.ts | 44 +- .../src/server/api/endpoints/admin/ad/delete.ts | 24 +- .../src/server/api/endpoints/admin/ad/list.ts | 30 +- .../src/server/api/endpoints/admin/ad/update.ts | 40 +- .../api/endpoints/admin/announcements/create.ts | 40 +- .../api/endpoints/admin/announcements/delete.ts | 24 +- .../api/endpoints/admin/announcements/list.ts | 61 +- .../api/endpoints/admin/announcements/update.ts | 34 +- .../server/api/endpoints/admin/delete-account.ts | 32 +- .../endpoints/admin/delete-all-files-of-a-user.ts | 32 +- .../api/endpoints/admin/drive-capacity-override.ts | 58 +- .../endpoints/admin/drive/clean-remote-files.ts | 18 +- .../server/api/endpoints/admin/drive/cleanup.ts | 32 +- .../src/server/api/endpoints/admin/drive/files.ts | 66 +- .../server/api/endpoints/admin/drive/show-file.ts | 52 +- .../api/endpoints/admin/emoji/add-aliases-bulk.ts | 47 +- .../src/server/api/endpoints/admin/emoji/add.ts | 104 +- .../src/server/api/endpoints/admin/emoji/copy.ts | 101 +- .../api/endpoints/admin/emoji/delete-bulk.ts | 47 +- .../src/server/api/endpoints/admin/emoji/delete.ts | 43 +- .../server/api/endpoints/admin/emoji/import-zip.ts | 19 +- .../api/endpoints/admin/emoji/list-remote.ts | 55 +- .../src/server/api/endpoints/admin/emoji/list.ts | 56 +- .../endpoints/admin/emoji/remove-aliases-bulk.ts | 47 +- .../api/endpoints/admin/emoji/set-aliases-bulk.ts | 41 +- .../api/endpoints/admin/emoji/set-category-bulk.ts | 41 +- .../src/server/api/endpoints/admin/emoji/update.ts | 43 +- .../endpoints/admin/federation/delete-all-files.ts | 32 +- .../federation/refresh-remote-instance-metadata.ts | 35 +- .../admin/federation/remove-all-following.ts | 47 +- .../endpoints/admin/federation/update-instance.ts | 36 +- .../server/api/endpoints/admin/get-index-stats.ts | 34 +- .../server/api/endpoints/admin/get-table-stats.ts | 43 +- .../src/server/api/endpoints/admin/get-user-ips.ts | 36 +- .../src/server/api/endpoints/admin/invite.ts | 50 +- .../backend/src/server/api/endpoints/admin/meta.ts | 192 +- .../server/api/endpoints/admin/moderators/add.ts | 50 +- .../api/endpoints/admin/moderators/remove.ts | 38 +- .../src/server/api/endpoints/admin/promo/create.ts | 48 +- .../src/server/api/endpoints/admin/queue/clear.ts | 23 +- .../api/endpoints/admin/queue/deliver-delayed.ts | 44 +- .../api/endpoints/admin/queue/inbox-delayed.ts | 44 +- .../src/server/api/endpoints/admin/queue/stats.ts | 42 +- .../src/server/api/endpoints/admin/relays/add.ts | 28 +- .../src/server/api/endpoints/admin/relays/list.ts | 18 +- .../server/api/endpoints/admin/relays/remove.ts | 18 +- .../server/api/endpoints/admin/reset-password.ts | 57 +- .../endpoints/admin/resolve-abuse-user-report.ts | 68 +- .../src/server/api/endpoints/admin/send-email.ts | 18 +- .../src/server/api/endpoints/admin/server-info.ts | 78 +- .../api/endpoints/admin/show-moderation-logs.ts | 28 +- .../src/server/api/endpoints/admin/show-user.ts | 112 +- .../src/server/api/endpoints/admin/show-users.ts | 84 +- .../src/server/api/endpoints/admin/silence-user.ts | 61 +- .../src/server/api/endpoints/admin/suspend-user.ts | 145 +- .../server/api/endpoints/admin/unsilence-user.ts | 53 +- .../server/api/endpoints/admin/unsuspend-user.ts | 53 +- .../src/server/api/endpoints/admin/update-meta.ts | 582 +++--- .../server/api/endpoints/admin/update-user-note.ts | 35 +- .../src/server/api/endpoints/admin/vacuum.ts | 36 - .../src/server/api/endpoints/announcements.ts | 55 +- .../src/server/api/endpoints/antennas/create.ts | 113 +- .../src/server/api/endpoints/antennas/delete.ts | 44 +- .../src/server/api/endpoints/antennas/list.ts | 29 +- .../src/server/api/endpoints/antennas/notes.ts | 108 +- .../src/server/api/endpoints/antennas/show.ts | 41 +- .../src/server/api/endpoints/antennas/update.ts | 126 +- .../backend/src/server/api/endpoints/ap/get.ts | 24 +- .../backend/src/server/api/endpoints/ap/show.ts | 167 +- .../backend/src/server/api/endpoints/app/create.ts | 70 +- .../backend/src/server/api/endpoints/app/show.ts | 45 +- .../src/server/api/endpoints/auth/accept.ts | 110 +- .../server/api/endpoints/auth/session/generate.ts | 70 +- .../src/server/api/endpoints/auth/session/show.ts | 39 +- .../server/api/endpoints/auth/session/userkey.ts | 104 +- .../src/server/api/endpoints/blocking/create.ts | 89 +- .../src/server/api/endpoints/blocking/delete.ts | 86 +- .../src/server/api/endpoints/blocking/list.ts | 40 +- .../src/server/api/endpoints/channels/create.ts | 69 +- .../src/server/api/endpoints/channels/featured.ts | 31 +- .../src/server/api/endpoints/channels/follow.ts | 54 +- .../src/server/api/endpoints/channels/followed.ts | 40 +- .../src/server/api/endpoints/channels/owned.ts | 40 +- .../src/server/api/endpoints/channels/show.ts | 37 +- .../src/server/api/endpoints/channels/timeline.ts | 80 +- .../src/server/api/endpoints/channels/unfollow.ts | 51 +- .../src/server/api/endpoints/channels/update.ts | 80 +- .../server/api/endpoints/charts/active-users.ts | 23 +- .../src/server/api/endpoints/charts/ap-request.ts | 23 +- .../src/server/api/endpoints/charts/drive.ts | 23 +- .../src/server/api/endpoints/charts/federation.ts | 23 +- .../src/server/api/endpoints/charts/hashtag.ts | 23 +- .../src/server/api/endpoints/charts/instance.ts | 23 +- .../src/server/api/endpoints/charts/notes.ts | 23 +- .../src/server/api/endpoints/charts/user/drive.ts | 23 +- .../server/api/endpoints/charts/user/following.ts | 23 +- .../src/server/api/endpoints/charts/user/notes.ts | 23 +- .../server/api/endpoints/charts/user/reactions.ts | 23 +- .../src/server/api/endpoints/charts/users.ts | 23 +- .../src/server/api/endpoints/clips/add-note.ts | 74 +- .../src/server/api/endpoints/clips/create.ts | 44 +- .../src/server/api/endpoints/clips/delete.ts | 36 +- .../backend/src/server/api/endpoints/clips/list.ts | 29 +- .../src/server/api/endpoints/clips/notes.ts | 99 +- .../src/server/api/endpoints/clips/remove-note.ts | 57 +- .../backend/src/server/api/endpoints/clips/show.ts | 45 +- .../src/server/api/endpoints/clips/update.ts | 51 +- packages/backend/src/server/api/endpoints/drive.ts | 37 +- .../src/server/api/endpoints/drive/files.ts | 58 +- .../api/endpoints/drive/files/attached-notes.ts | 54 +- .../api/endpoints/drive/files/check-existence.ts | 30 +- .../src/server/api/endpoints/drive/files/create.ts | 106 +- .../src/server/api/endpoints/drive/files/delete.ts | 47 +- .../api/endpoints/drive/files/find-by-hash.ts | 33 +- .../src/server/api/endpoints/drive/files/find.ts | 35 +- .../src/server/api/endpoints/drive/files/show.ts | 73 +- .../src/server/api/endpoints/drive/files/update.ts | 101 +- .../api/endpoints/drive/files/upload-from-url.ts | 39 +- .../src/server/api/endpoints/drive/folders.ts | 46 +- .../server/api/endpoints/drive/folders/create.ts | 77 +- .../server/api/endpoints/drive/folders/delete.ts | 63 +- .../src/server/api/endpoints/drive/folders/find.ts | 35 +- .../src/server/api/endpoints/drive/folders/show.ts | 45 +- .../server/api/endpoints/drive/folders/update.ts | 132 +- .../src/server/api/endpoints/drive/stream.ts | 48 +- .../api/endpoints/email-address/available.ts | 18 +- .../backend/src/server/api/endpoints/endpoint.ts | 29 +- .../backend/src/server/api/endpoints/endpoints.ts | 15 +- .../server/api/endpoints/export-custom-emojis.ts | 18 +- .../server/api/endpoints/federation/followers.ts | 36 +- .../server/api/endpoints/federation/following.ts | 36 +- .../server/api/endpoints/federation/instances.ts | 177 +- .../api/endpoints/federation/show-instance.ts | 30 +- .../src/server/api/endpoints/federation/stats.ts | 102 +- .../api/endpoints/federation/update-remote-user.ts | 23 +- .../src/server/api/endpoints/federation/users.ts | 36 +- .../backend/src/server/api/endpoints/fetch-rss.ts | 48 +- .../src/server/api/endpoints/following/create.ts | 98 +- .../src/server/api/endpoints/following/delete.ts | 80 +- .../server/api/endpoints/following/invalidate.ts | 80 +- .../api/endpoints/following/requests/accept.ts | 43 +- .../api/endpoints/following/requests/cancel.ts | 60 +- .../api/endpoints/following/requests/list.ts | 29 +- .../api/endpoints/following/requests/reject.ts | 37 +- .../src/server/api/endpoints/gallery/featured.ts | 33 +- .../src/server/api/endpoints/gallery/popular.ts | 31 +- .../src/server/api/endpoints/gallery/posts.ts | 32 +- .../server/api/endpoints/gallery/posts/create.ts | 71 +- .../server/api/endpoints/gallery/posts/delete.ts | 36 +- .../src/server/api/endpoints/gallery/posts/like.ts | 71 +- .../src/server/api/endpoints/gallery/posts/show.ts | 37 +- .../server/api/endpoints/gallery/posts/unlike.ts | 49 +- .../server/api/endpoints/gallery/posts/update.ts | 70 +- .../server/api/endpoints/get-online-users-count.ts | 30 +- .../src/server/api/endpoints/hashtags/list.ts | 79 +- .../src/server/api/endpoints/hashtags/search.ts | 34 +- .../src/server/api/endpoints/hashtags/show.ts | 33 +- .../src/server/api/endpoints/hashtags/trend.ts | 184 +- .../src/server/api/endpoints/hashtags/users.ts | 67 +- packages/backend/src/server/api/endpoints/i.ts | 33 +- .../backend/src/server/api/endpoints/i/2fa/done.ts | 60 +- .../src/server/api/endpoints/i/2fa/key-done.ts | 248 +-- .../server/api/endpoints/i/2fa/password-less.ts | 24 +- .../src/server/api/endpoints/i/2fa/register-key.ts | 90 +- .../src/server/api/endpoints/i/2fa/register.ts | 85 +- .../src/server/api/endpoints/i/2fa/remove-key.ts | 70 +- .../src/server/api/endpoints/i/2fa/unregister.ts | 38 +- .../backend/src/server/api/endpoints/i/apps.ts | 52 +- .../src/server/api/endpoints/i/authorized-apps.ts | 51 +- .../src/server/api/endpoints/i/change-password.ts | 48 +- .../src/server/api/endpoints/i/delete-account.ts | 47 +- .../src/server/api/endpoints/i/export-blocking.ts | 18 +- .../src/server/api/endpoints/i/export-following.ts | 18 +- .../src/server/api/endpoints/i/export-mute.ts | 18 +- .../src/server/api/endpoints/i/export-notes.ts | 18 +- .../server/api/endpoints/i/export-user-lists.ts | 18 +- .../src/server/api/endpoints/i/favorites.ts | 42 +- .../src/server/api/endpoints/i/gallery/likes.ts | 40 +- .../src/server/api/endpoints/i/gallery/posts.ts | 40 +- .../api/endpoints/i/get-word-muted-notes-count.ts | 30 +- .../src/server/api/endpoints/i/import-blocking.ts | 34 +- .../src/server/api/endpoints/i/import-following.ts | 34 +- .../src/server/api/endpoints/i/import-muting.ts | 34 +- .../server/api/endpoints/i/import-user-lists.ts | 34 +- .../src/server/api/endpoints/i/notifications.ts | 222 ++- .../src/server/api/endpoints/i/page-likes.ts | 40 +- .../backend/src/server/api/endpoints/i/pages.ts | 40 +- packages/backend/src/server/api/endpoints/i/pin.ts | 40 +- .../api/endpoints/i/read-all-messaging-messages.ts | 65 +- .../api/endpoints/i/read-all-unread-notes.ts | 36 +- .../server/api/endpoints/i/read-announcement.ts | 75 +- .../src/server/api/endpoints/i/regenerate-token.ts | 65 +- .../src/server/api/endpoints/i/registry/get-all.ts | 44 +- .../server/api/endpoints/i/registry/get-detail.ts | 48 +- .../src/server/api/endpoints/i/registry/get.ts | 42 +- .../api/endpoints/i/registry/keys-with-type.ts | 46 +- .../src/server/api/endpoints/i/registry/keys.ts | 32 +- .../src/server/api/endpoints/i/registry/remove.ts | 42 +- .../src/server/api/endpoints/i/registry/scopes.ts | 46 +- .../src/server/api/endpoints/i/registry/set.ts | 85 +- .../src/server/api/endpoints/i/revoke-token.ts | 38 +- .../src/server/api/endpoints/i/signin-history.ts | 32 +- .../backend/src/server/api/endpoints/i/unpin.ts | 36 +- .../src/server/api/endpoints/i/update-email.ts | 122 +- .../backend/src/server/api/endpoints/i/update.ts | 273 +-- .../server/api/endpoints/i/user-group-invites.ts | 38 +- .../src/server/api/endpoints/i/webhooks/create.ts | 55 +- .../src/server/api/endpoints/i/webhooks/delete.ts | 46 +- .../src/server/api/endpoints/i/webhooks/list.ts | 26 +- .../src/server/api/endpoints/i/webhooks/show.ts | 36 +- .../src/server/api/endpoints/i/webhooks/update.ts | 58 +- .../src/server/api/endpoints/messaging/history.ts | 131 +- .../src/server/api/endpoints/messaging/messages.ts | 171 +- .../api/endpoints/messaging/messages/create.ts | 154 +- .../api/endpoints/messaging/messages/delete.ts | 40 +- .../api/endpoints/messaging/messages/read.ts | 52 +- packages/backend/src/server/api/endpoints/meta.ts | 226 ++- .../src/server/api/endpoints/miauth/gen-token.ts | 62 +- .../src/server/api/endpoints/mute/create.ts | 107 +- .../src/server/api/endpoints/mute/delete.ts | 71 +- .../backend/src/server/api/endpoints/mute/list.ts | 40 +- .../backend/src/server/api/endpoints/my/apps.ts | 47 +- packages/backend/src/server/api/endpoints/notes.ts | 106 +- .../src/server/api/endpoints/notes/children.ts | 85 +- .../src/server/api/endpoints/notes/clips.ts | 57 +- .../src/server/api/endpoints/notes/conversation.ts | 74 +- .../src/server/api/endpoints/notes/create.ts | 234 ++- .../src/server/api/endpoints/notes/delete.ts | 45 +- .../server/api/endpoints/notes/favorites/create.ts | 69 +- .../server/api/endpoints/notes/favorites/delete.ts | 56 +- .../src/server/api/endpoints/notes/featured.ts | 81 +- .../server/api/endpoints/notes/global-timeline.ts | 112 +- .../server/api/endpoints/notes/hybrid-timeline.ts | 193 +- .../server/api/endpoints/notes/local-timeline.ts | 137 +- .../src/server/api/endpoints/notes/mentions.ts | 101 +- .../api/endpoints/notes/polls/recommendation.ts | 130 +- .../src/server/api/endpoints/notes/polls/vote.ts | 204 +- .../src/server/api/endpoints/notes/reactions.ts | 66 +- .../server/api/endpoints/notes/reactions/create.ts | 39 +- .../server/api/endpoints/notes/reactions/delete.ts | 35 +- .../src/server/api/endpoints/notes/renotes.ts | 74 +- .../src/server/api/endpoints/notes/replies.ts | 69 +- .../server/api/endpoints/notes/search-by-tag.ts | 145 +- .../src/server/api/endpoints/notes/search.ts | 151 +- .../backend/src/server/api/endpoints/notes/show.ts | 40 +- .../src/server/api/endpoints/notes/state.ts | 74 +- .../api/endpoints/notes/thread-muting/create.ts | 73 +- .../api/endpoints/notes/thread-muting/delete.ts | 40 +- .../src/server/api/endpoints/notes/timeline.ts | 185 +- .../src/server/api/endpoints/notes/translate.ts | 129 +- .../src/server/api/endpoints/notes/unrenote.ts | 52 +- .../api/endpoints/notes/user-list-timeline.ts | 166 +- .../server/api/endpoints/notes/watching/create.ts | 38 - .../server/api/endpoints/notes/watching/delete.ts | 38 - .../server/api/endpoints/notifications/create.ts | 28 +- .../endpoints/notifications/mark-all-as-read.ts | 45 +- .../src/server/api/endpoints/notifications/read.ts | 20 +- .../backend/src/server/api/endpoints/page-push.ts | 51 +- .../src/server/api/endpoints/pages/create.ts | 101 +- .../src/server/api/endpoints/pages/delete.ts | 34 +- .../src/server/api/endpoints/pages/featured.ts | 33 +- .../backend/src/server/api/endpoints/pages/like.ts | 71 +- .../backend/src/server/api/endpoints/pages/show.ts | 64 +- .../src/server/api/endpoints/pages/unlike.ts | 49 +- .../src/server/api/endpoints/pages/update.ts | 107 +- packages/backend/src/server/api/endpoints/ping.ts | 19 +- .../src/server/api/endpoints/pinned-users.ts | 38 +- .../backend/src/server/api/endpoints/promo/read.ts | 63 +- .../server/api/endpoints/request-reset-password.ts | 107 +- .../backend/src/server/api/endpoints/reset-db.ts | 34 +- .../src/server/api/endpoints/reset-password.ts | 57 +- .../src/server/api/endpoints/server-info.ts | 45 +- packages/backend/src/server/api/endpoints/stats.ts | 84 +- .../src/server/api/endpoints/sw/register.ts | 77 +- .../src/server/api/endpoints/sw/unregister.ts | 26 +- packages/backend/src/server/api/endpoints/test.ts | 15 +- .../src/server/api/endpoints/username/available.ts | 46 +- packages/backend/src/server/api/endpoints/users.ts | 85 +- .../src/server/api/endpoints/users/clips.ts | 38 +- .../src/server/api/endpoints/users/followers.ts | 95 +- .../src/server/api/endpoints/users/following.ts | 95 +- .../server/api/endpoints/users/gallery/posts.ts | 36 +- .../users/get-frequently-replied-users.ts | 133 +- .../server/api/endpoints/users/groups/create.ts | 63 +- .../server/api/endpoints/users/groups/delete.ts | 36 +- .../endpoints/users/groups/invitations/accept.ts | 65 +- .../endpoints/users/groups/invitations/reject.ts | 44 +- .../server/api/endpoints/users/groups/invite.ts | 126 +- .../server/api/endpoints/users/groups/joined.ts | 48 +- .../src/server/api/endpoints/users/groups/leave.ts | 45 +- .../src/server/api/endpoints/users/groups/owned.ts | 31 +- .../src/server/api/endpoints/users/groups/pull.ts | 61 +- .../src/server/api/endpoints/users/groups/show.ts | 56 +- .../server/api/endpoints/users/groups/transfer.ts | 85 +- .../server/api/endpoints/users/groups/update.ts | 47 +- .../src/server/api/endpoints/users/lists/create.ts | 42 +- .../src/server/api/endpoints/users/lists/delete.ts | 36 +- .../src/server/api/endpoints/users/lists/list.ts | 31 +- .../src/server/api/endpoints/users/lists/pull.ts | 64 +- .../src/server/api/endpoints/users/lists/push.ts | 103 +- .../src/server/api/endpoints/users/lists/show.ts | 41 +- .../src/server/api/endpoints/users/lists/update.ts | 47 +- .../src/server/api/endpoints/users/notes.ts | 144 +- .../src/server/api/endpoints/users/pages.ts | 38 +- .../src/server/api/endpoints/users/reactions.ts | 54 +- .../server/api/endpoints/users/recommendation.ts | 64 +- .../src/server/api/endpoints/users/relation.ts | 27 +- .../src/server/api/endpoints/users/report-abuse.ts | 129 +- .../endpoints/users/search-by-username-and-host.ts | 168 +- .../src/server/api/endpoints/users/search.ts | 186 +- .../backend/src/server/api/endpoints/users/show.ts | 124 +- .../src/server/api/endpoints/users/stats.ts | 187 +- packages/backend/src/server/api/error.ts | 16 +- packages/backend/src/server/api/index.ts | 126 -- .../server/api/integration/DiscordServerService.ts | 315 +++ .../server/api/integration/GithubServerService.ts | 287 +++ .../server/api/integration/TwitterServerService.ts | 230 +++ packages/backend/src/server/api/limiter.ts | 77 - packages/backend/src/server/api/logger.ts | 3 - .../backend/src/server/api/openapi/gen-spec.ts | 190 -- packages/backend/src/server/api/private/signin.ts | 250 --- .../src/server/api/private/signup-pending.ts | 35 - packages/backend/src/server/api/private/signup.ts | 112 -- packages/backend/src/server/api/service/discord.ts | 287 --- packages/backend/src/server/api/service/github.ts | 259 --- packages/backend/src/server/api/service/twitter.ts | 201 -- .../src/server/api/stream/ChannelsService.ts | 62 + packages/backend/src/server/api/stream/channel.ts | 2 +- .../src/server/api/stream/channels/admin.ts | 20 +- .../src/server/api/stream/channels/antenna.ts | 38 +- .../src/server/api/stream/channels/channel.ts | 50 +- .../src/server/api/stream/channels/drive.ts | 20 +- .../server/api/stream/channels/global-timeline.ts | 47 +- .../src/server/api/stream/channels/hashtag.ts | 38 +- .../server/api/stream/channels/home-timeline.ts | 42 +- .../server/api/stream/channels/hybrid-timeline.ts | 54 +- .../src/server/api/stream/channels/index.ts | 33 - .../server/api/stream/channels/local-timeline.ts | 47 +- .../backend/src/server/api/stream/channels/main.ts | 40 +- .../server/api/stream/channels/messaging-index.ts | 20 +- .../src/server/api/stream/channels/messaging.ts | 75 +- .../src/server/api/stream/channels/queue-stats.ts | 20 +- .../src/server/api/stream/channels/server-stats.ts | 20 +- .../src/server/api/stream/channels/user-list.ts | 60 +- packages/backend/src/server/api/stream/index.ts | 68 +- packages/backend/src/server/api/stream/types.ts | 35 +- packages/backend/src/server/api/streaming.ts | 67 - packages/backend/src/server/assets/bad-egg.png | Bin 0 -> 1676 bytes .../backend/src/server/assets/cache-expired.png | Bin 0 -> 6048 bytes packages/backend/src/server/assets/dummy.png | Bin 0 -> 6285 bytes .../backend/src/server/assets/not-an-image.png | Bin 0 -> 2780 bytes .../src/server/assets/thumbnail-not-available.png | Bin 0 -> 5705 bytes packages/backend/src/server/assets/tombstone.png | Bin 0 -> 5028 bytes .../backend/src/server/file/assets/bad-egg.png | Bin 1676 -> 0 bytes .../src/server/file/assets/cache-expired.png | Bin 6048 -> 0 bytes packages/backend/src/server/file/assets/dummy.png | Bin 6285 -> 0 bytes .../src/server/file/assets/not-an-image.png | Bin 2780 -> 0 bytes .../server/file/assets/thumbnail-not-available.png | Bin 5705 -> 0 bytes .../backend/src/server/file/assets/tombstone.png | Bin 5028 -> 0 bytes packages/backend/src/server/file/index.ts | 40 - .../backend/src/server/file/send-drive-file.ts | 126 -- packages/backend/src/server/index.ts | 168 -- packages/backend/src/server/nodeinfo.ts | 104 - packages/backend/src/server/proxy/index.ts | 26 - packages/backend/src/server/proxy/proxy-media.ts | 98 - .../backend/src/server/web/ClientServerService.ts | 594 ++++++ packages/backend/src/server/web/FeedService.ts | 86 + .../backend/src/server/web/UrlPreviewService.ts | 84 + packages/backend/src/server/web/feed.ts | 58 - packages/backend/src/server/web/index.ts | 521 ----- packages/backend/src/server/web/manifest.ts | 18 - packages/backend/src/server/web/url-preview.ts | 65 - packages/backend/src/server/well-known.ts | 151 -- .../backend/src/services/add-note-to-antenna.ts | 54 - packages/backend/src/services/blocking/create.ts | 145 -- packages/backend/src/services/blocking/delete.ts | 34 - .../src/services/chart/charts/active-users.ts | 44 - .../src/services/chart/charts/ap-request.ts | 38 - .../backend/src/services/chart/charts/drive.ts | 38 - .../services/chart/charts/entities/active-users.ts | 17 - .../services/chart/charts/entities/ap-request.ts | 11 - .../src/services/chart/charts/entities/drive.ts | 16 - .../services/chart/charts/entities/federation.ts | 16 - .../src/services/chart/charts/entities/hashtag.ts | 10 - .../src/services/chart/charts/entities/instance.ts | 32 - .../src/services/chart/charts/entities/notes.ts | 22 - .../chart/charts/entities/per-user-drive.ts | 14 - .../chart/charts/entities/per-user-following.ts | 20 - .../chart/charts/entities/per-user-notes.ts | 15 - .../chart/charts/entities/per-user-reactions.ts | 10 - .../services/chart/charts/entities/test-grouped.ts | 11 - .../chart/charts/entities/test-intersection.ts | 11 - .../services/chart/charts/entities/test-unique.ts | 9 - .../src/services/chart/charts/entities/test.ts | 11 - .../src/services/chart/charts/entities/users.ts | 14 - .../src/services/chart/charts/federation.ts | 103 - .../backend/src/services/chart/charts/hashtag.ts | 29 - .../backend/src/services/chart/charts/instance.ts | 103 - .../backend/src/services/chart/charts/notes.ts | 45 - .../src/services/chart/charts/per-user-drive.ts | 42 - .../services/chart/charts/per-user-following.ts | 56 - .../src/services/chart/charts/per-user-notes.ts | 41 - .../services/chart/charts/per-user-reactions.ts | 30 - .../src/services/chart/charts/test-grouped.ts | 35 - .../src/services/chart/charts/test-intersection.ts | 32 - .../src/services/chart/charts/test-unique.ts | 26 - packages/backend/src/services/chart/charts/test.ts | 42 - .../backend/src/services/chart/charts/users.ts | 41 - packages/backend/src/services/chart/core.ts | 677 ------- packages/backend/src/services/chart/entities.ts | 39 - packages/backend/src/services/chart/index.ts | 51 - .../backend/src/services/create-notification.ts | 62 - .../backend/src/services/create-system-user.ts | 68 - packages/backend/src/services/delete-account.ts | 23 - packages/backend/src/services/detect-sensitive.ts | 48 - packages/backend/src/services/drive/add-file.ts | 540 ----- packages/backend/src/services/drive/delete-file.ts | 101 - .../src/services/drive/generate-video-thumbnail.ts | 29 - .../backend/src/services/drive/image-processor.ts | 87 - .../backend/src/services/drive/internal-storage.ts | 34 - packages/backend/src/services/drive/logger.ts | 3 - packages/backend/src/services/drive/s3.ts | 24 - .../backend/src/services/drive/upload-from-url.ts | 68 - .../src/services/fetch-instance-metadata.ts | 268 --- packages/backend/src/services/following/create.ts | 201 -- packages/backend/src/services/following/delete.ts | 83 - packages/backend/src/services/following/reject.ts | 122 -- .../src/services/following/requests/accept-all.ts | 18 - .../src/services/following/requests/accept.ts | 31 - .../src/services/following/requests/cancel.ts | 36 - .../src/services/following/requests/create.ts | 63 - packages/backend/src/services/i/pin.ts | 92 - packages/backend/src/services/i/update.ts | 19 - .../backend/src/services/insert-moderation-log.ts | 13 - packages/backend/src/services/instance-actor.ts | 28 - packages/backend/src/services/logger.ts | 128 -- packages/backend/src/services/messages/create.ts | 108 - packages/backend/src/services/messages/delete.ts | 30 - packages/backend/src/services/note/create.ts | 692 ------- packages/backend/src/services/note/delete.ts | 141 -- packages/backend/src/services/note/polls/update.ts | 21 - packages/backend/src/services/note/polls/vote.ts | 81 - .../backend/src/services/note/reaction/create.ts | 145 -- .../backend/src/services/note/reaction/delete.ts | 58 - packages/backend/src/services/note/read.ts | 132 -- packages/backend/src/services/note/unread.ts | 55 - packages/backend/src/services/note/unwatch.ts | 10 - packages/backend/src/services/note/watch.ts | 20 - packages/backend/src/services/push-notification.ts | 85 - .../src/services/register-or-fetch-instance-doc.ts | 31 - packages/backend/src/services/relay.ts | 101 - .../src/services/send-email-notification.ts | 36 - packages/backend/src/services/send-email.ts | 122 -- packages/backend/src/services/stream.ts | 116 -- packages/backend/src/services/suspend-user.ts | 37 - packages/backend/src/services/unsuspend-user.ts | 38 - packages/backend/src/services/update-hashtag.ts | 128 -- packages/backend/src/services/user-cache.ts | 44 - packages/backend/src/services/user-list/push.ts | 27 - .../src/services/validate-email-for-account.ts | 37 - packages/backend/test/.eslintrc.cjs | 2 +- packages/backend/test/_e2e/api-visibility.ts | 477 +++++ packages/backend/test/_e2e/api.ts | 83 + packages/backend/test/_e2e/block.ts | 85 + packages/backend/test/_e2e/endpoints.ts | 881 ++++++++ packages/backend/test/_e2e/fetch-resource.ts | 205 ++ packages/backend/test/_e2e/ff-visibility.ts | 167 ++ packages/backend/test/_e2e/mute.ts | 123 ++ packages/backend/test/_e2e/note.ts | 370 ++++ packages/backend/test/_e2e/streaming.ts | 545 +++++ packages/backend/test/_e2e/thread-mute.ts | 103 + packages/backend/test/_e2e/user-notes.ts | 61 + packages/backend/test/activitypub.ts | 96 - packages/backend/test/ap-request.ts | 55 - packages/backend/test/api-visibility.ts | 476 ----- packages/backend/test/api.ts | 83 - packages/backend/test/block.ts | 85 - packages/backend/test/chart.ts | 531 ----- packages/backend/test/endpoints.ts | 865 -------- packages/backend/test/extract-mentions.ts | 42 - packages/backend/test/fetch-resource.ts | 205 -- packages/backend/test/ff-visibility.ts | 167 -- packages/backend/test/get-file-info.ts | 191 -- packages/backend/test/mfm.ts | 89 - packages/backend/test/mute.ts | 123 -- packages/backend/test/note.ts | 370 ---- packages/backend/test/prelude/maybe.ts | 2 +- packages/backend/test/prelude/url.ts | 2 +- packages/backend/test/reaction-lib.ts | 83 - packages/backend/test/streaming.ts | 545 ----- packages/backend/test/tests/activitypub.ts | 89 + packages/backend/test/tests/ap-request.ts | 55 + packages/backend/test/tests/extract-mentions.ts | 42 + packages/backend/test/tests/mfm.ts | 89 + packages/backend/test/tests/reaction-lib.ts | 83 + packages/backend/test/thread-mute.ts | 103 - packages/backend/test/tsconfig.json | 3 +- packages/backend/test/unit/FileInfoService.ts | 237 +++ packages/backend/test/unit/RelayService.ts | 96 + packages/backend/test/unit/chart.ts | 574 ++++++ packages/backend/test/user-notes.ts | 61 - packages/backend/test/utils.ts | 94 +- packages/backend/yarn.lock | 2094 +++++++++++++++++--- packages/client/.eslintrc.js | 3 + packages/client/src/scripts/aiscript/api.ts | 2 +- packages/client/src/scripts/hpml/type-checker.ts | 12 +- packages/shared/.eslintrc.js | 5 + packages/sw/src/types.ts | 2 +- 1091 files changed, 52548 insertions(+), 40168 deletions(-) delete mode 100644 packages/backend/.mocharc.json create mode 100644 packages/backend/jest-resolver.cjs create mode 100644 packages/backend/jest.config.cjs create mode 100644 packages/backend/src/AppModule.ts create mode 100644 packages/backend/src/GlobalModule.ts create mode 100644 packages/backend/src/RepositoryModule.ts create mode 100644 packages/backend/src/config.ts delete mode 100644 packages/backend/src/config/index.ts delete mode 100644 packages/backend/src/config/load.ts delete mode 100644 packages/backend/src/config/types.ts create mode 100644 packages/backend/src/core/AccountUpdateService.ts create mode 100644 packages/backend/src/core/AiService.ts create mode 100644 packages/backend/src/core/AntennaService.ts create mode 100644 packages/backend/src/core/AppLockService.ts create mode 100644 packages/backend/src/core/CaptchaService.ts create mode 100644 packages/backend/src/core/CoreModule.ts create mode 100644 packages/backend/src/core/CreateNotificationService.ts create mode 100644 packages/backend/src/core/CreateSystemUserService.ts create mode 100644 packages/backend/src/core/CustomEmojiService.ts create mode 100644 packages/backend/src/core/DeleteAccountService.ts create mode 100644 packages/backend/src/core/DownloadService.ts create mode 100644 packages/backend/src/core/DriveService.ts create mode 100644 packages/backend/src/core/EmailService.ts create mode 100644 packages/backend/src/core/FederatedInstanceService.ts create mode 100644 packages/backend/src/core/FetchInstanceMetadataService.ts create mode 100644 packages/backend/src/core/FileInfoService.ts create mode 100644 packages/backend/src/core/GlobalEventService.ts create mode 100644 packages/backend/src/core/HashtagService.ts create mode 100644 packages/backend/src/core/HttpRequestService.ts create mode 100644 packages/backend/src/core/IdService.ts create mode 100644 packages/backend/src/core/ImageProcessingService.ts create mode 100644 packages/backend/src/core/InstanceActorService.ts create mode 100644 packages/backend/src/core/InternalStorageService.ts create mode 100644 packages/backend/src/core/MessagingService.ts create mode 100644 packages/backend/src/core/MetaService.ts create mode 100644 packages/backend/src/core/MfmService.ts create mode 100644 packages/backend/src/core/ModerationLogService.ts create mode 100644 packages/backend/src/core/NoteCreateService.ts create mode 100644 packages/backend/src/core/NoteDeleteService.ts create mode 100644 packages/backend/src/core/NotePiningService.ts create mode 100644 packages/backend/src/core/NoteReadService.ts create mode 100644 packages/backend/src/core/NotificationService.ts create mode 100644 packages/backend/src/core/PollService.ts create mode 100644 packages/backend/src/core/ProxyAccountService.ts create mode 100644 packages/backend/src/core/PushNotificationService.ts create mode 100644 packages/backend/src/core/QueryService.ts create mode 100644 packages/backend/src/core/QueueService.ts create mode 100644 packages/backend/src/core/ReactionService.ts create mode 100644 packages/backend/src/core/RelayService.ts create mode 100644 packages/backend/src/core/S3Service.ts create mode 100644 packages/backend/src/core/SignupService.ts create mode 100644 packages/backend/src/core/TwoFactorAuthenticationService.ts create mode 100644 packages/backend/src/core/UserBlockingService.ts create mode 100644 packages/backend/src/core/UserCacheService.ts create mode 100644 packages/backend/src/core/UserFollowingService.ts create mode 100644 packages/backend/src/core/UserKeypairStoreService.ts create mode 100644 packages/backend/src/core/UserListService.ts create mode 100644 packages/backend/src/core/UserMutingService.ts create mode 100644 packages/backend/src/core/UserSuspendService.ts create mode 100644 packages/backend/src/core/UtilityService.ts create mode 100644 packages/backend/src/core/VideoProcessingService.ts create mode 100644 packages/backend/src/core/WebhookService.ts create mode 100644 packages/backend/src/core/chart/ChartManagementService.ts create mode 100644 packages/backend/src/core/chart/charts/active-users.ts create mode 100644 packages/backend/src/core/chart/charts/ap-request.ts create mode 100644 packages/backend/src/core/chart/charts/drive.ts create mode 100644 packages/backend/src/core/chart/charts/entities/active-users.ts create mode 100644 packages/backend/src/core/chart/charts/entities/ap-request.ts create mode 100644 packages/backend/src/core/chart/charts/entities/drive.ts create mode 100644 packages/backend/src/core/chart/charts/entities/federation.ts create mode 100644 packages/backend/src/core/chart/charts/entities/hashtag.ts create mode 100644 packages/backend/src/core/chart/charts/entities/instance.ts create mode 100644 packages/backend/src/core/chart/charts/entities/notes.ts create mode 100644 packages/backend/src/core/chart/charts/entities/per-user-drive.ts create mode 100644 packages/backend/src/core/chart/charts/entities/per-user-following.ts create mode 100644 packages/backend/src/core/chart/charts/entities/per-user-notes.ts create mode 100644 packages/backend/src/core/chart/charts/entities/per-user-reactions.ts create mode 100644 packages/backend/src/core/chart/charts/entities/test-grouped.ts create mode 100644 packages/backend/src/core/chart/charts/entities/test-intersection.ts create mode 100644 packages/backend/src/core/chart/charts/entities/test-unique.ts create mode 100644 packages/backend/src/core/chart/charts/entities/test.ts create mode 100644 packages/backend/src/core/chart/charts/entities/users.ts create mode 100644 packages/backend/src/core/chart/charts/federation.ts create mode 100644 packages/backend/src/core/chart/charts/hashtag.ts create mode 100644 packages/backend/src/core/chart/charts/instance.ts create mode 100644 packages/backend/src/core/chart/charts/notes.ts create mode 100644 packages/backend/src/core/chart/charts/per-user-drive.ts create mode 100644 packages/backend/src/core/chart/charts/per-user-following.ts create mode 100644 packages/backend/src/core/chart/charts/per-user-notes.ts create mode 100644 packages/backend/src/core/chart/charts/per-user-reactions.ts create mode 100644 packages/backend/src/core/chart/charts/test-grouped.ts create mode 100644 packages/backend/src/core/chart/charts/test-intersection.ts create mode 100644 packages/backend/src/core/chart/charts/test-unique.ts create mode 100644 packages/backend/src/core/chart/charts/test.ts create mode 100644 packages/backend/src/core/chart/charts/users.ts create mode 100644 packages/backend/src/core/chart/core.ts create mode 100644 packages/backend/src/core/chart/entities.ts create mode 100644 packages/backend/src/core/entities/AbuseUserReportEntityService.ts create mode 100644 packages/backend/src/core/entities/AntennaEntityService.ts create mode 100644 packages/backend/src/core/entities/AppEntityService.ts create mode 100644 packages/backend/src/core/entities/AuthSessionEntityService.ts create mode 100644 packages/backend/src/core/entities/BlockingEntityService.ts create mode 100644 packages/backend/src/core/entities/ChannelEntityService.ts create mode 100644 packages/backend/src/core/entities/ClipEntityService.ts create mode 100644 packages/backend/src/core/entities/DriveFileEntityService.ts create mode 100644 packages/backend/src/core/entities/DriveFolderEntityService.ts create mode 100644 packages/backend/src/core/entities/EmojiEntityService.ts create mode 100644 packages/backend/src/core/entities/FollowRequestEntityService.ts create mode 100644 packages/backend/src/core/entities/FollowingEntityService.ts create mode 100644 packages/backend/src/core/entities/GalleryLikeEntityService.ts create mode 100644 packages/backend/src/core/entities/GalleryPostEntityService.ts create mode 100644 packages/backend/src/core/entities/HashtagEntityService.ts create mode 100644 packages/backend/src/core/entities/InstanceEntityService.ts create mode 100644 packages/backend/src/core/entities/MessagingMessageEntityService.ts create mode 100644 packages/backend/src/core/entities/ModerationLogEntityService.ts create mode 100644 packages/backend/src/core/entities/MutingEntityService.ts create mode 100644 packages/backend/src/core/entities/NoteEntityService.ts create mode 100644 packages/backend/src/core/entities/NoteFavoriteEntityService.ts create mode 100644 packages/backend/src/core/entities/NoteReactionEntityService.ts create mode 100644 packages/backend/src/core/entities/NotificationEntityService.ts create mode 100644 packages/backend/src/core/entities/PageEntityService.ts create mode 100644 packages/backend/src/core/entities/PageLikeEntityService.ts create mode 100644 packages/backend/src/core/entities/SigninEntityService.ts create mode 100644 packages/backend/src/core/entities/UserEntityService.ts create mode 100644 packages/backend/src/core/entities/UserGroupEntityService.ts create mode 100644 packages/backend/src/core/entities/UserGroupInvitationEntityService.ts create mode 100644 packages/backend/src/core/entities/UserListEntityService.ts create mode 100644 packages/backend/src/core/queue/QueueModule.ts create mode 100644 packages/backend/src/core/remote/RemoteLoggerService.ts create mode 100644 packages/backend/src/core/remote/ResolveUserService.ts create mode 100644 packages/backend/src/core/remote/WebfingerService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApAudienceService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApDbResolverService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApInboxService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApLoggerService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApMfmService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApRendererService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApRequestService.ts create mode 100644 packages/backend/src/core/remote/activitypub/ApResolverService.ts create mode 100644 packages/backend/src/core/remote/activitypub/LdSignatureService.ts create mode 100644 packages/backend/src/core/remote/activitypub/misc/contexts.ts create mode 100644 packages/backend/src/core/remote/activitypub/misc/get-note-html.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/ApImageService.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/ApMentionService.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/ApNoteService.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/ApPersonService.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/icon.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/identifier.ts create mode 100644 packages/backend/src/core/remote/activitypub/models/tag.ts create mode 100644 packages/backend/src/core/remote/activitypub/type.ts create mode 100644 packages/backend/src/daemons/DaemonModule.ts create mode 100644 packages/backend/src/daemons/JanitorService.ts create mode 100644 packages/backend/src/daemons/QueueStatsService.ts create mode 100644 packages/backend/src/daemons/ServerStatsService.ts delete mode 100644 packages/backend/src/daemons/janitor.ts delete mode 100644 packages/backend/src/daemons/queue-stats.ts delete mode 100644 packages/backend/src/daemons/server-stats.ts delete mode 100644 packages/backend/src/db/elasticsearch.ts delete mode 100644 packages/backend/src/db/logger.ts delete mode 100644 packages/backend/src/db/postgre.ts delete mode 100644 packages/backend/src/db/redis.ts create mode 100644 packages/backend/src/di-symbols.ts create mode 100644 packages/backend/src/logger.ts delete mode 100644 packages/backend/src/mfm/from-html.ts delete mode 100644 packages/backend/src/mfm/to-html.ts delete mode 100644 packages/backend/src/misc/antenna-cache.ts delete mode 100644 packages/backend/src/misc/app-lock.ts delete mode 100644 packages/backend/src/misc/captcha.ts delete mode 100644 packages/backend/src/misc/check-hit-antenna.ts delete mode 100644 packages/backend/src/misc/convert-host.ts delete mode 100644 packages/backend/src/misc/count-same-renotes.ts delete mode 100644 packages/backend/src/misc/detect-url-mime.ts delete mode 100644 packages/backend/src/misc/download-text-file.ts delete mode 100644 packages/backend/src/misc/download-url.ts delete mode 100644 packages/backend/src/misc/fetch-meta.ts delete mode 100644 packages/backend/src/misc/fetch-proxy-account.ts delete mode 100644 packages/backend/src/misc/fetch.ts delete mode 100644 packages/backend/src/misc/gen-id.ts create mode 100644 packages/backend/src/misc/generate-native-user-token.ts delete mode 100644 packages/backend/src/misc/get-file-info.ts create mode 100644 packages/backend/src/misc/is-native-token.ts delete mode 100644 packages/backend/src/misc/keypair-store.ts delete mode 100644 packages/backend/src/misc/populate-emojis.ts create mode 100644 packages/backend/src/misc/prelude/README.md create mode 100644 packages/backend/src/misc/prelude/array.ts create mode 100644 packages/backend/src/misc/prelude/await-all.ts create mode 100644 packages/backend/src/misc/prelude/math.ts create mode 100644 packages/backend/src/misc/prelude/maybe.ts create mode 100644 packages/backend/src/misc/prelude/relation.ts create mode 100644 packages/backend/src/misc/prelude/string.ts create mode 100644 packages/backend/src/misc/prelude/symbol.ts create mode 100644 packages/backend/src/misc/prelude/time.ts create mode 100644 packages/backend/src/misc/prelude/url.ts create mode 100644 packages/backend/src/misc/prelude/xml.ts delete mode 100644 packages/backend/src/misc/reaction-lib.ts create mode 100644 packages/backend/src/misc/reset-db.ts create mode 100644 packages/backend/src/misc/status-error.ts delete mode 100644 packages/backend/src/misc/webhook-cache.ts create mode 100644 packages/backend/src/models/entities/AbuseUserReport.ts create mode 100644 packages/backend/src/models/entities/AccessToken.ts create mode 100644 packages/backend/src/models/entities/Ad.ts create mode 100644 packages/backend/src/models/entities/Announcement.ts create mode 100644 packages/backend/src/models/entities/AnnouncementRead.ts create mode 100644 packages/backend/src/models/entities/Antenna.ts create mode 100644 packages/backend/src/models/entities/AntennaNote.ts create mode 100644 packages/backend/src/models/entities/App.ts create mode 100644 packages/backend/src/models/entities/AttestationChallenge.ts create mode 100644 packages/backend/src/models/entities/AuthSession.ts create mode 100644 packages/backend/src/models/entities/Blocking.ts create mode 100644 packages/backend/src/models/entities/Channel.ts create mode 100644 packages/backend/src/models/entities/ChannelFollowing.ts create mode 100644 packages/backend/src/models/entities/ChannelNotePining.ts create mode 100644 packages/backend/src/models/entities/Clip.ts create mode 100644 packages/backend/src/models/entities/ClipNote.ts create mode 100644 packages/backend/src/models/entities/DriveFile.ts create mode 100644 packages/backend/src/models/entities/DriveFolder.ts create mode 100644 packages/backend/src/models/entities/Emoji.ts create mode 100644 packages/backend/src/models/entities/FollowRequest.ts create mode 100644 packages/backend/src/models/entities/Following.ts create mode 100644 packages/backend/src/models/entities/GalleryLike.ts create mode 100644 packages/backend/src/models/entities/GalleryPost.ts create mode 100644 packages/backend/src/models/entities/Hashtag.ts create mode 100644 packages/backend/src/models/entities/Instance.ts create mode 100644 packages/backend/src/models/entities/MessagingMessage.ts create mode 100644 packages/backend/src/models/entities/Meta.ts create mode 100644 packages/backend/src/models/entities/ModerationLog.ts create mode 100644 packages/backend/src/models/entities/MutedNote.ts create mode 100644 packages/backend/src/models/entities/Muting.ts create mode 100644 packages/backend/src/models/entities/Note.ts create mode 100644 packages/backend/src/models/entities/NoteFavorite.ts create mode 100644 packages/backend/src/models/entities/NoteReaction.ts create mode 100644 packages/backend/src/models/entities/NoteThreadMuting.ts create mode 100644 packages/backend/src/models/entities/NoteUnread.ts create mode 100644 packages/backend/src/models/entities/Notification.ts create mode 100644 packages/backend/src/models/entities/Page.ts create mode 100644 packages/backend/src/models/entities/PageLike.ts create mode 100644 packages/backend/src/models/entities/PasswordResetRequest.ts create mode 100644 packages/backend/src/models/entities/Poll.ts create mode 100644 packages/backend/src/models/entities/PollVote.ts create mode 100644 packages/backend/src/models/entities/PromoNote.ts create mode 100644 packages/backend/src/models/entities/PromoRead.ts create mode 100644 packages/backend/src/models/entities/RegistrationTickets.ts create mode 100644 packages/backend/src/models/entities/RegistryItem.ts create mode 100644 packages/backend/src/models/entities/Relay.ts create mode 100644 packages/backend/src/models/entities/Signin.ts create mode 100644 packages/backend/src/models/entities/SwSubscription.ts create mode 100644 packages/backend/src/models/entities/UsedUsername.ts create mode 100644 packages/backend/src/models/entities/User.ts create mode 100644 packages/backend/src/models/entities/UserGroup.ts create mode 100644 packages/backend/src/models/entities/UserGroupInvitation.ts create mode 100644 packages/backend/src/models/entities/UserGroupJoining.ts create mode 100644 packages/backend/src/models/entities/UserIp.ts create mode 100644 packages/backend/src/models/entities/UserKeypair.ts create mode 100644 packages/backend/src/models/entities/UserList.ts create mode 100644 packages/backend/src/models/entities/UserListJoining.ts create mode 100644 packages/backend/src/models/entities/UserNotePining.ts create mode 100644 packages/backend/src/models/entities/UserPending.ts create mode 100644 packages/backend/src/models/entities/UserProfile.ts create mode 100644 packages/backend/src/models/entities/UserPublickey.ts create mode 100644 packages/backend/src/models/entities/UserSecurityKey.ts create mode 100644 packages/backend/src/models/entities/Webhook.ts delete mode 100644 packages/backend/src/models/entities/abuse-user-report.ts delete mode 100644 packages/backend/src/models/entities/access-token.ts delete mode 100644 packages/backend/src/models/entities/ad.ts delete mode 100644 packages/backend/src/models/entities/announcement-read.ts delete mode 100644 packages/backend/src/models/entities/announcement.ts delete mode 100644 packages/backend/src/models/entities/antenna-note.ts delete mode 100644 packages/backend/src/models/entities/antenna.ts delete mode 100644 packages/backend/src/models/entities/app.ts delete mode 100644 packages/backend/src/models/entities/attestation-challenge.ts delete mode 100644 packages/backend/src/models/entities/auth-session.ts delete mode 100644 packages/backend/src/models/entities/blocking.ts delete mode 100644 packages/backend/src/models/entities/channel-following.ts delete mode 100644 packages/backend/src/models/entities/channel-note-pining.ts delete mode 100644 packages/backend/src/models/entities/channel.ts delete mode 100644 packages/backend/src/models/entities/clip-note.ts delete mode 100644 packages/backend/src/models/entities/clip.ts delete mode 100644 packages/backend/src/models/entities/drive-file.ts delete mode 100644 packages/backend/src/models/entities/drive-folder.ts delete mode 100644 packages/backend/src/models/entities/emoji.ts delete mode 100644 packages/backend/src/models/entities/follow-request.ts delete mode 100644 packages/backend/src/models/entities/following.ts delete mode 100644 packages/backend/src/models/entities/gallery-like.ts delete mode 100644 packages/backend/src/models/entities/gallery-post.ts delete mode 100644 packages/backend/src/models/entities/hashtag.ts delete mode 100644 packages/backend/src/models/entities/instance.ts delete mode 100644 packages/backend/src/models/entities/messaging-message.ts delete mode 100644 packages/backend/src/models/entities/meta.ts delete mode 100644 packages/backend/src/models/entities/moderation-log.ts delete mode 100644 packages/backend/src/models/entities/muted-note.ts delete mode 100644 packages/backend/src/models/entities/muting.ts delete mode 100644 packages/backend/src/models/entities/note-favorite.ts delete mode 100644 packages/backend/src/models/entities/note-reaction.ts delete mode 100644 packages/backend/src/models/entities/note-thread-muting.ts delete mode 100644 packages/backend/src/models/entities/note-unread.ts delete mode 100644 packages/backend/src/models/entities/note-watching.ts delete mode 100644 packages/backend/src/models/entities/note.ts delete mode 100644 packages/backend/src/models/entities/notification.ts delete mode 100644 packages/backend/src/models/entities/page-like.ts delete mode 100644 packages/backend/src/models/entities/page.ts delete mode 100644 packages/backend/src/models/entities/password-reset-request.ts delete mode 100644 packages/backend/src/models/entities/poll-vote.ts delete mode 100644 packages/backend/src/models/entities/poll.ts delete mode 100644 packages/backend/src/models/entities/promo-note.ts delete mode 100644 packages/backend/src/models/entities/promo-read.ts delete mode 100644 packages/backend/src/models/entities/registration-tickets.ts delete mode 100644 packages/backend/src/models/entities/registry-item.ts delete mode 100644 packages/backend/src/models/entities/relay.ts delete mode 100644 packages/backend/src/models/entities/signin.ts delete mode 100644 packages/backend/src/models/entities/sw-subscription.ts delete mode 100644 packages/backend/src/models/entities/used-username.ts delete mode 100644 packages/backend/src/models/entities/user-group-invitation.ts delete mode 100644 packages/backend/src/models/entities/user-group-joining.ts delete mode 100644 packages/backend/src/models/entities/user-group.ts delete mode 100644 packages/backend/src/models/entities/user-ip.ts delete mode 100644 packages/backend/src/models/entities/user-keypair.ts delete mode 100644 packages/backend/src/models/entities/user-list-joining.ts delete mode 100644 packages/backend/src/models/entities/user-list.ts delete mode 100644 packages/backend/src/models/entities/user-note-pining.ts delete mode 100644 packages/backend/src/models/entities/user-pending.ts delete mode 100644 packages/backend/src/models/entities/user-profile.ts delete mode 100644 packages/backend/src/models/entities/user-publickey.ts delete mode 100644 packages/backend/src/models/entities/user-security-key.ts delete mode 100644 packages/backend/src/models/entities/user.ts delete mode 100644 packages/backend/src/models/entities/webhook.ts delete mode 100644 packages/backend/src/models/repositories/abuse-user-report.ts delete mode 100644 packages/backend/src/models/repositories/antenna.ts delete mode 100644 packages/backend/src/models/repositories/app.ts delete mode 100644 packages/backend/src/models/repositories/auth-session.ts delete mode 100644 packages/backend/src/models/repositories/blocking.ts delete mode 100644 packages/backend/src/models/repositories/channel.ts delete mode 100644 packages/backend/src/models/repositories/clip.ts delete mode 100644 packages/backend/src/models/repositories/drive-file.ts delete mode 100644 packages/backend/src/models/repositories/drive-folder.ts delete mode 100644 packages/backend/src/models/repositories/emoji.ts delete mode 100644 packages/backend/src/models/repositories/follow-request.ts delete mode 100644 packages/backend/src/models/repositories/following.ts delete mode 100644 packages/backend/src/models/repositories/gallery-like.ts delete mode 100644 packages/backend/src/models/repositories/gallery-post.ts delete mode 100644 packages/backend/src/models/repositories/hashtag.ts delete mode 100644 packages/backend/src/models/repositories/instance.ts delete mode 100644 packages/backend/src/models/repositories/messaging-message.ts delete mode 100644 packages/backend/src/models/repositories/moderation-logs.ts delete mode 100644 packages/backend/src/models/repositories/muting.ts delete mode 100644 packages/backend/src/models/repositories/note-favorite.ts delete mode 100644 packages/backend/src/models/repositories/note-reaction.ts delete mode 100644 packages/backend/src/models/repositories/note.ts delete mode 100644 packages/backend/src/models/repositories/notification.ts delete mode 100644 packages/backend/src/models/repositories/page-like.ts delete mode 100644 packages/backend/src/models/repositories/page.ts delete mode 100644 packages/backend/src/models/repositories/relay.ts delete mode 100644 packages/backend/src/models/repositories/signin.ts delete mode 100644 packages/backend/src/models/repositories/user-group-invitation.ts delete mode 100644 packages/backend/src/models/repositories/user-group.ts delete mode 100644 packages/backend/src/models/repositories/user-list.ts delete mode 100644 packages/backend/src/models/repositories/user.ts create mode 100644 packages/backend/src/postgre.ts delete mode 100644 packages/backend/src/prelude/README.md delete mode 100644 packages/backend/src/prelude/array.ts delete mode 100644 packages/backend/src/prelude/await-all.ts delete mode 100644 packages/backend/src/prelude/math.ts delete mode 100644 packages/backend/src/prelude/maybe.ts delete mode 100644 packages/backend/src/prelude/relation.ts delete mode 100644 packages/backend/src/prelude/string.ts delete mode 100644 packages/backend/src/prelude/symbol.ts delete mode 100644 packages/backend/src/prelude/time.ts delete mode 100644 packages/backend/src/prelude/url.ts delete mode 100644 packages/backend/src/prelude/xml.ts create mode 100644 packages/backend/src/queue/DbQueueProcessorsService.ts create mode 100644 packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts create mode 100644 packages/backend/src/queue/QueueLoggerService.ts create mode 100644 packages/backend/src/queue/QueueProcessorModule.ts create mode 100644 packages/backend/src/queue/QueueProcessorService.ts create mode 100644 packages/backend/src/queue/SystemQueueProcessorsService.ts delete mode 100644 packages/backend/src/queue/index.ts delete mode 100644 packages/backend/src/queue/initialize.ts delete mode 100644 packages/backend/src/queue/logger.ts create mode 100644 packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/CleanChartsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/CleanProcessorService.ts create mode 100644 packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts create mode 100644 packages/backend/src/queue/processors/DeleteAccountProcessorService.ts create mode 100644 packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts create mode 100644 packages/backend/src/queue/processors/DeleteFileProcessorService.ts create mode 100644 packages/backend/src/queue/processors/DeliverProcessorService.ts create mode 100644 packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportBlockingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportFollowingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportMutingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportNotesProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ExportUserListsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportBlockingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportFollowingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportMutingProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportUserListsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/InboxProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ResyncChartsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/TickChartsProcessorService.ts create mode 100644 packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts delete mode 100644 packages/backend/src/queue/processors/db/delete-account.ts delete mode 100644 packages/backend/src/queue/processors/db/delete-drive-files.ts delete mode 100644 packages/backend/src/queue/processors/db/export-blocking.ts delete mode 100644 packages/backend/src/queue/processors/db/export-custom-emojis.ts delete mode 100644 packages/backend/src/queue/processors/db/export-following.ts delete mode 100644 packages/backend/src/queue/processors/db/export-mute.ts delete mode 100644 packages/backend/src/queue/processors/db/export-notes.ts delete mode 100644 packages/backend/src/queue/processors/db/export-user-lists.ts delete mode 100644 packages/backend/src/queue/processors/db/import-blocking.ts delete mode 100644 packages/backend/src/queue/processors/db/import-custom-emojis.ts delete mode 100644 packages/backend/src/queue/processors/db/import-following.ts delete mode 100644 packages/backend/src/queue/processors/db/import-muting.ts delete mode 100644 packages/backend/src/queue/processors/db/import-user-lists.ts delete mode 100644 packages/backend/src/queue/processors/db/index.ts delete mode 100644 packages/backend/src/queue/processors/deliver.ts delete mode 100644 packages/backend/src/queue/processors/ended-poll-notification.ts delete mode 100644 packages/backend/src/queue/processors/inbox.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/clean-remote-files.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/delete-file.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/index.ts delete mode 100644 packages/backend/src/queue/processors/system/check-expired-mutings.ts delete mode 100644 packages/backend/src/queue/processors/system/clean-charts.ts delete mode 100644 packages/backend/src/queue/processors/system/clean.ts delete mode 100644 packages/backend/src/queue/processors/system/index.ts delete mode 100644 packages/backend/src/queue/processors/system/resync-charts.ts delete mode 100644 packages/backend/src/queue/processors/system/tick-charts.ts delete mode 100644 packages/backend/src/queue/processors/webhook-deliver.ts delete mode 100644 packages/backend/src/queue/queues.ts create mode 100644 packages/backend/src/redis.ts delete mode 100644 packages/backend/src/remote/activitypub/ap-request.ts delete mode 100644 packages/backend/src/remote/activitypub/audience.ts delete mode 100644 packages/backend/src/remote/activitypub/db-resolver.ts delete mode 100644 packages/backend/src/remote/activitypub/deliver-manager.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/accept/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/accept/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/add/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/announce/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/announce/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/block/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/create/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/create/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/actor.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/flag/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/like.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/read.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/reject/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/reject/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/remove/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/accept.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/announce.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/block.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/like.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/update/index.ts delete mode 100644 packages/backend/src/remote/activitypub/logger.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/contexts.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/get-note-html.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/html-to-mfm.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/ld-signature.ts delete mode 100644 packages/backend/src/remote/activitypub/models/icon.ts delete mode 100644 packages/backend/src/remote/activitypub/models/identifier.ts delete mode 100644 packages/backend/src/remote/activitypub/models/image.ts delete mode 100644 packages/backend/src/remote/activitypub/models/mention.ts delete mode 100644 packages/backend/src/remote/activitypub/models/note.ts delete mode 100644 packages/backend/src/remote/activitypub/models/person.ts delete mode 100644 packages/backend/src/remote/activitypub/models/question.ts delete mode 100644 packages/backend/src/remote/activitypub/models/tag.ts delete mode 100644 packages/backend/src/remote/activitypub/perform.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/accept.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/add.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/announce.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/block.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/create.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/delete.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/document.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/emoji.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/flag.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow-relay.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow-user.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/hashtag.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/image.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/index.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/key.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/like.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/mention.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/note.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/person.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/question.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/read.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/reject.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/remove.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/tombstone.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/undo.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/update.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/vote.ts delete mode 100644 packages/backend/src/remote/activitypub/request.ts delete mode 100644 packages/backend/src/remote/activitypub/resolver.ts delete mode 100644 packages/backend/src/remote/activitypub/type.ts delete mode 100644 packages/backend/src/remote/logger.ts delete mode 100644 packages/backend/src/remote/resolve-user.ts delete mode 100644 packages/backend/src/remote/webfinger.ts create mode 100644 packages/backend/src/server/ActivityPubServerService.ts create mode 100644 packages/backend/src/server/FileServerService.ts create mode 100644 packages/backend/src/server/MediaProxyServerService.ts create mode 100644 packages/backend/src/server/NodeinfoServerService.ts create mode 100644 packages/backend/src/server/ServerModule.ts create mode 100644 packages/backend/src/server/ServerService.ts create mode 100644 packages/backend/src/server/WellKnownServerService.ts delete mode 100644 packages/backend/src/server/activitypub.ts delete mode 100644 packages/backend/src/server/activitypub/featured.ts delete mode 100644 packages/backend/src/server/activitypub/followers.ts delete mode 100644 packages/backend/src/server/activitypub/following.ts delete mode 100644 packages/backend/src/server/activitypub/outbox.ts delete mode 100644 packages/backend/src/server/api/2fa.ts create mode 100644 packages/backend/src/server/api/ApiCallService.ts create mode 100644 packages/backend/src/server/api/ApiLoggerService.ts create mode 100644 packages/backend/src/server/api/ApiServerService.ts create mode 100644 packages/backend/src/server/api/AuthenticateService.ts create mode 100644 packages/backend/src/server/api/EndpointsModule.ts create mode 100644 packages/backend/src/server/api/RateLimiterService.ts create mode 100644 packages/backend/src/server/api/SigninApiService.ts create mode 100644 packages/backend/src/server/api/SigninService.ts create mode 100644 packages/backend/src/server/api/SignupApiService.ts create mode 100644 packages/backend/src/server/api/StreamingApiServerService.ts delete mode 100644 packages/backend/src/server/api/api-handler.ts delete mode 100644 packages/backend/src/server/api/authenticate.ts delete mode 100644 packages/backend/src/server/api/call.ts create mode 100644 packages/backend/src/server/api/common/GetterService.ts delete mode 100644 packages/backend/src/server/api/common/generate-block-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-channel-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-note-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-note-thread-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-user-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-native-user-token.ts delete mode 100644 packages/backend/src/server/api/common/generate-replies-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-visibility-query.ts delete mode 100644 packages/backend/src/server/api/common/getters.ts delete mode 100644 packages/backend/src/server/api/common/is-native-token.ts delete mode 100644 packages/backend/src/server/api/common/make-pagination-query.ts delete mode 100644 packages/backend/src/server/api/common/read-messaging-message.ts delete mode 100644 packages/backend/src/server/api/common/read-notification.ts delete mode 100644 packages/backend/src/server/api/common/signin.ts delete mode 100644 packages/backend/src/server/api/common/signup.ts delete mode 100644 packages/backend/src/server/api/define.ts create mode 100644 packages/backend/src/server/api/endpoint-base.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/vacuum.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/watching/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/watching/delete.ts delete mode 100644 packages/backend/src/server/api/index.ts create mode 100644 packages/backend/src/server/api/integration/DiscordServerService.ts create mode 100644 packages/backend/src/server/api/integration/GithubServerService.ts create mode 100644 packages/backend/src/server/api/integration/TwitterServerService.ts delete mode 100644 packages/backend/src/server/api/limiter.ts delete mode 100644 packages/backend/src/server/api/logger.ts delete mode 100644 packages/backend/src/server/api/openapi/gen-spec.ts delete mode 100644 packages/backend/src/server/api/private/signin.ts delete mode 100644 packages/backend/src/server/api/private/signup-pending.ts delete mode 100644 packages/backend/src/server/api/private/signup.ts delete mode 100644 packages/backend/src/server/api/service/discord.ts delete mode 100644 packages/backend/src/server/api/service/github.ts delete mode 100644 packages/backend/src/server/api/service/twitter.ts create mode 100644 packages/backend/src/server/api/stream/ChannelsService.ts delete mode 100644 packages/backend/src/server/api/stream/channels/index.ts delete mode 100644 packages/backend/src/server/api/streaming.ts create mode 100644 packages/backend/src/server/assets/bad-egg.png create mode 100644 packages/backend/src/server/assets/cache-expired.png create mode 100644 packages/backend/src/server/assets/dummy.png create mode 100644 packages/backend/src/server/assets/not-an-image.png create mode 100644 packages/backend/src/server/assets/thumbnail-not-available.png create mode 100644 packages/backend/src/server/assets/tombstone.png delete mode 100644 packages/backend/src/server/file/assets/bad-egg.png delete mode 100644 packages/backend/src/server/file/assets/cache-expired.png delete mode 100644 packages/backend/src/server/file/assets/dummy.png delete mode 100644 packages/backend/src/server/file/assets/not-an-image.png delete mode 100644 packages/backend/src/server/file/assets/thumbnail-not-available.png delete mode 100644 packages/backend/src/server/file/assets/tombstone.png delete mode 100644 packages/backend/src/server/file/index.ts delete mode 100644 packages/backend/src/server/file/send-drive-file.ts delete mode 100644 packages/backend/src/server/index.ts delete mode 100644 packages/backend/src/server/nodeinfo.ts delete mode 100644 packages/backend/src/server/proxy/index.ts delete mode 100644 packages/backend/src/server/proxy/proxy-media.ts create mode 100644 packages/backend/src/server/web/ClientServerService.ts create mode 100644 packages/backend/src/server/web/FeedService.ts create mode 100644 packages/backend/src/server/web/UrlPreviewService.ts delete mode 100644 packages/backend/src/server/web/feed.ts delete mode 100644 packages/backend/src/server/web/index.ts delete mode 100644 packages/backend/src/server/web/manifest.ts delete mode 100644 packages/backend/src/server/web/url-preview.ts delete mode 100644 packages/backend/src/server/well-known.ts delete mode 100644 packages/backend/src/services/add-note-to-antenna.ts delete mode 100644 packages/backend/src/services/blocking/create.ts delete mode 100644 packages/backend/src/services/blocking/delete.ts delete mode 100644 packages/backend/src/services/chart/charts/active-users.ts delete mode 100644 packages/backend/src/services/chart/charts/ap-request.ts delete mode 100644 packages/backend/src/services/chart/charts/drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/active-users.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/ap-request.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/federation.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/hashtag.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/instance.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/notes.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-following.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-notes.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-reactions.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-grouped.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-intersection.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-unique.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/users.ts delete mode 100644 packages/backend/src/services/chart/charts/federation.ts delete mode 100644 packages/backend/src/services/chart/charts/hashtag.ts delete mode 100644 packages/backend/src/services/chart/charts/instance.ts delete mode 100644 packages/backend/src/services/chart/charts/notes.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-drive.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-following.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-notes.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-reactions.ts delete mode 100644 packages/backend/src/services/chart/charts/test-grouped.ts delete mode 100644 packages/backend/src/services/chart/charts/test-intersection.ts delete mode 100644 packages/backend/src/services/chart/charts/test-unique.ts delete mode 100644 packages/backend/src/services/chart/charts/test.ts delete mode 100644 packages/backend/src/services/chart/charts/users.ts delete mode 100644 packages/backend/src/services/chart/core.ts delete mode 100644 packages/backend/src/services/chart/entities.ts delete mode 100644 packages/backend/src/services/chart/index.ts delete mode 100644 packages/backend/src/services/create-notification.ts delete mode 100644 packages/backend/src/services/create-system-user.ts delete mode 100644 packages/backend/src/services/delete-account.ts delete mode 100644 packages/backend/src/services/detect-sensitive.ts delete mode 100644 packages/backend/src/services/drive/add-file.ts delete mode 100644 packages/backend/src/services/drive/delete-file.ts delete mode 100644 packages/backend/src/services/drive/generate-video-thumbnail.ts delete mode 100644 packages/backend/src/services/drive/image-processor.ts delete mode 100644 packages/backend/src/services/drive/internal-storage.ts delete mode 100644 packages/backend/src/services/drive/logger.ts delete mode 100644 packages/backend/src/services/drive/s3.ts delete mode 100644 packages/backend/src/services/drive/upload-from-url.ts delete mode 100644 packages/backend/src/services/fetch-instance-metadata.ts delete mode 100644 packages/backend/src/services/following/create.ts delete mode 100644 packages/backend/src/services/following/delete.ts delete mode 100644 packages/backend/src/services/following/reject.ts delete mode 100644 packages/backend/src/services/following/requests/accept-all.ts delete mode 100644 packages/backend/src/services/following/requests/accept.ts delete mode 100644 packages/backend/src/services/following/requests/cancel.ts delete mode 100644 packages/backend/src/services/following/requests/create.ts delete mode 100644 packages/backend/src/services/i/pin.ts delete mode 100644 packages/backend/src/services/i/update.ts delete mode 100644 packages/backend/src/services/insert-moderation-log.ts delete mode 100644 packages/backend/src/services/instance-actor.ts delete mode 100644 packages/backend/src/services/logger.ts delete mode 100644 packages/backend/src/services/messages/create.ts delete mode 100644 packages/backend/src/services/messages/delete.ts delete mode 100644 packages/backend/src/services/note/create.ts delete mode 100644 packages/backend/src/services/note/delete.ts delete mode 100644 packages/backend/src/services/note/polls/update.ts delete mode 100644 packages/backend/src/services/note/polls/vote.ts delete mode 100644 packages/backend/src/services/note/reaction/create.ts delete mode 100644 packages/backend/src/services/note/reaction/delete.ts delete mode 100644 packages/backend/src/services/note/read.ts delete mode 100644 packages/backend/src/services/note/unread.ts delete mode 100644 packages/backend/src/services/note/unwatch.ts delete mode 100644 packages/backend/src/services/note/watch.ts delete mode 100644 packages/backend/src/services/push-notification.ts delete mode 100644 packages/backend/src/services/register-or-fetch-instance-doc.ts delete mode 100644 packages/backend/src/services/relay.ts delete mode 100644 packages/backend/src/services/send-email-notification.ts delete mode 100644 packages/backend/src/services/send-email.ts delete mode 100644 packages/backend/src/services/stream.ts delete mode 100644 packages/backend/src/services/suspend-user.ts delete mode 100644 packages/backend/src/services/unsuspend-user.ts delete mode 100644 packages/backend/src/services/update-hashtag.ts delete mode 100644 packages/backend/src/services/user-cache.ts delete mode 100644 packages/backend/src/services/user-list/push.ts delete mode 100644 packages/backend/src/services/validate-email-for-account.ts create mode 100644 packages/backend/test/_e2e/api-visibility.ts create mode 100644 packages/backend/test/_e2e/api.ts create mode 100644 packages/backend/test/_e2e/block.ts create mode 100644 packages/backend/test/_e2e/endpoints.ts create mode 100644 packages/backend/test/_e2e/fetch-resource.ts create mode 100644 packages/backend/test/_e2e/ff-visibility.ts create mode 100644 packages/backend/test/_e2e/mute.ts create mode 100644 packages/backend/test/_e2e/note.ts create mode 100644 packages/backend/test/_e2e/streaming.ts create mode 100644 packages/backend/test/_e2e/thread-mute.ts create mode 100644 packages/backend/test/_e2e/user-notes.ts delete mode 100644 packages/backend/test/activitypub.ts delete mode 100644 packages/backend/test/ap-request.ts delete mode 100644 packages/backend/test/api-visibility.ts delete mode 100644 packages/backend/test/api.ts delete mode 100644 packages/backend/test/block.ts delete mode 100644 packages/backend/test/chart.ts delete mode 100644 packages/backend/test/endpoints.ts delete mode 100644 packages/backend/test/extract-mentions.ts delete mode 100644 packages/backend/test/fetch-resource.ts delete mode 100644 packages/backend/test/ff-visibility.ts delete mode 100644 packages/backend/test/get-file-info.ts delete mode 100644 packages/backend/test/mfm.ts delete mode 100644 packages/backend/test/mute.ts delete mode 100644 packages/backend/test/note.ts delete mode 100644 packages/backend/test/reaction-lib.ts delete mode 100644 packages/backend/test/streaming.ts create mode 100644 packages/backend/test/tests/activitypub.ts create mode 100644 packages/backend/test/tests/ap-request.ts create mode 100644 packages/backend/test/tests/extract-mentions.ts create mode 100644 packages/backend/test/tests/mfm.ts create mode 100644 packages/backend/test/tests/reaction-lib.ts delete mode 100644 packages/backend/test/thread-mute.ts create mode 100644 packages/backend/test/unit/FileInfoService.ts create mode 100644 packages/backend/test/unit/RelayService.ts create mode 100644 packages/backend/test/unit/chart.ts delete mode 100644 packages/backend/test/user-notes.ts (limited to 'packages/backend/src/server/api/endpoints/users/notes.ts') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c32c82e2a1..bb35cce137 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: pull_request: jobs: - mocha: + jest: runs-on: ubuntu-latest strategy: @@ -49,7 +49,11 @@ jobs: - name: Build run: yarn build - name: Test - run: yarn mocha + run: yarn jest-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + files: ./packages/backend/coverage/coverage-final.json e2e: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4547138eb4..9a4c93e9fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,7 +124,7 @@ npm run test #### Run specify test ``` -npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT="./test/tsconfig.json" npx mocha test/foo.ts --require ts-node/register +npm run jest -- foo.ts ``` ### e2e tests diff --git a/package.json b/package.json index 37f0762f5a..efb749c288 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,10 @@ "cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "cypress run", "e2e": "start-server-and-test start:test http://localhost:61812 cy:run", - "mocha": "cd packages/backend && cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha", - "test": "npm run mocha", + "jest": "cd packages/backend && cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --detectOpenHandles --runInBand", + "jest-and-coverage": "cd packages/backend && cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --detectOpenHandles --runInBand", + "test": "npm run jest", + "test-and-coverage": "npm run jest-and-coverage", "format": "gulp format", "clean": "node ./scripts/clean.js", "clean-all": "node ./scripts/clean-all.js", diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json deleted file mode 100644 index f836f9e900..0000000000 --- a/packages/backend/.mocharc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extension": ["ts","js","cjs","mjs"], - "node-option": [ - "experimental-specifier-resolution=node", - "loader=./test/loader.js" - ], - "slow": 1000, - "timeout": 30000, - "exit": true -} diff --git a/packages/backend/jest-resolver.cjs b/packages/backend/jest-resolver.cjs new file mode 100644 index 0000000000..4424b800dc --- /dev/null +++ b/packages/backend/jest-resolver.cjs @@ -0,0 +1,14 @@ +// https://github.com/facebook/jest/issues/12270#issuecomment-1194746382 + +const nativeModule = require('node:module'); + +function resolver(module, options) { + const { basedir, defaultResolver } = options; + try { + return defaultResolver(module, options); + } catch (error) { + return nativeModule.createRequire(basedir).resolve(module); + } +} + +module.exports = resolver; diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs new file mode 100644 index 0000000000..4f85bd36bd --- /dev/null +++ b/packages/backend/jest.config.cjs @@ -0,0 +1,214 @@ +/* +* For a detailed explanation regarding each configuration property and type check, visit: +* https://jestjs.io/docs/en/configuration.html +*/ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest", + + // Automatically clear mock calls and instances between every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ['src/**/*.ts'], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + globals: { + "ts-jest": { + "useESM": true, + tsconfig: "test/tsconfig.json", + diagnostics: { + exclude: ['**'], + }, + } + }, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + moduleNameMapper: { + "^@/(.*?).js": "/src/$1.ts", + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest/presets/js-with-ts-esm", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + resolver: './jest-resolver.cjs', + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + roots: [ + "" + ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/unit/**/*.ts", + //"/test/e2e/**/*.ts" + ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + transform: { + "": [ + "ts-jest", + { + "useESM": true + } + ] + }, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "\\\\node_modules\\\\", + // "\\.pnp\\.[^\\\\]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, + + extensionsToTreatAsEsm: ['.ts'], +}; diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index a4e903abad..32c26f7b67 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,6 +1,8 @@ import { DataSource } from 'typeorm'; -import config from './built/config/index.js'; -import { entities } from './built/db/postgre.js'; +import { loadConfig } from './built/config.js'; +import { entities } from './built/postgre.js'; + +const config = loadConfig(); export default new DataSource({ type: 'postgres', diff --git a/packages/backend/package.json b/packages/backend/package.json index be8283e4a0..30bf9faa32 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,13 +1,14 @@ { - "main": "./index.js", "private": true, "type": "module", "scripts": { "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", "lint": "eslint --quiet \"src/**/*.ts\"", - "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", - "test": "npm run mocha" + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --detectOpenHandles --runInBand", + "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --detectOpenHandles --runInBand", + "test": "npm run jest", + "test-and-coverage": "npm run jest-and-coverage" }, "resolutions": { "chokidar": "^3.3.1", @@ -23,9 +24,13 @@ "@koa/cors": "3.1.0", "@koa/multer": "3.0.0", "@koa/router": "9.0.1", + "@nestjs/common": "9.0.11", + "@nestjs/core": "9.0.11", + "@nestjs/testing": "9.0.11", "@peertube/http-signature": "1.7.0", "@sinonjs/fake-timers": "9.1.2", "@syuilo/aiscript": "0.11.1", + "@types/pg": "8.6.5", "ajv": "8.11.0", "archiver": "5.3.1", "autobind-decorator": "2.4.0", @@ -71,7 +76,6 @@ "mfm-js": "0.23.0", "mime-types": "2.1.35", "misskey-js": "0.0.14", - "mocha": "10.0.0", "ms": "3.0.0-canary.1", "multer": "1.4.4", "nested-property": "4.0.0", @@ -96,6 +100,7 @@ "rename": "1.0.4", "rndstr": "1.0.0", "rss-parser": "3.12.0", + "rxjs": "7.5.6", "s-age": "1.1.2", "sanitize-html": "2.7.1", "semver": "7.3.7", @@ -129,6 +134,7 @@ "@types/cbor": "6.0.0", "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.20", + "@types/jest": "29.0.0", "@types/js-yaml": "4.0.5", "@types/jsdom": "20.0.0", "@types/jsonld": "1.5.6", @@ -144,7 +150,6 @@ "@types/koa__cors": "3.1.1", "@types/koa__multer": "2.0.4", "@types/koa__router": "8.0.11", - "@types/mocha": "9.1.1", "@types/node": "18.7.16", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.5", @@ -173,6 +178,8 @@ "eslint": "8.23.0", "eslint-plugin-import": "2.26.0", "execa": "6.1.0", + "jest": "29.0.1", + "ts-jest": "28.0.8", "typescript": "4.8.3" } } diff --git a/packages/backend/src/AppModule.ts b/packages/backend/src/AppModule.ts new file mode 100644 index 0000000000..14f48576a7 --- /dev/null +++ b/packages/backend/src/AppModule.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { CoreModule } from '@/core/CoreModule.js'; +import { ServerModule } from '@/server/ServerModule.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; + +@Module({ + imports: [ + GlobalModule, + CoreModule, + ServerModule, + QueueProcessorModule, + ], +}) +export class AppModule {} diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts new file mode 100644 index 0000000000..dbc5c698df --- /dev/null +++ b/packages/backend/src/GlobalModule.ts @@ -0,0 +1,63 @@ +import { Global, Inject, Module } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import { DataSource } from 'typeorm'; +import { createRedisConnection } from '@/redis.js'; +import { DI } from './di-symbols.js'; +import { loadConfig } from './config.js'; +import { createPostgreDataSource } from './postgre.js'; +import { RepositoryModule } from './RepositoryModule.js'; +import type { Provider, OnApplicationShutdown } from '@nestjs/common'; + +const config = loadConfig(); + +const $config: Provider = { + provide: DI.config, + useValue: config, +}; + +const $db: Provider = { + provide: DI.db, + useFactory: async () => { + const db = createPostgreDataSource(); + return await db.initialize(); + }, +}; + +const $redis: Provider = { + provide: DI.redis, + useFactory: () => { + const redisClient = createRedisConnection(); + return redisClient; + }, +}; + +const $redisSubscriber: Provider = { + provide: DI.redisSubscriber, + useFactory: () => { + const redisSubscriber = createRedisConnection(); + redisSubscriber.subscribe(config.host); + return redisSubscriber; + }, +}; + +@Global() +@Module({ + imports: [RepositoryModule], + providers: [$config, $db, $redis, $redisSubscriber], + exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule], +}) +export class GlobalModule implements OnApplicationShutdown { + constructor( + @Inject(DI.db) private db: DataSource, + @Inject(DI.redis) private redisClient: Redis, + @Inject(DI.redisSubscriber) private redisSubscriber: Redis, + ) {} + + async onApplicationShutdown(signal: string): Promise { + await Promise.all([ + this.db.destroy(), + this.redisClient.disconnect(), + this.redisSubscriber.disconnect(), + ]); + } +} diff --git a/packages/backend/src/RepositoryModule.ts b/packages/backend/src/RepositoryModule.ts new file mode 100644 index 0000000000..0e3ef58992 --- /dev/null +++ b/packages/backend/src/RepositoryModule.ts @@ -0,0 +1,519 @@ +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 } from './models/index.js'; +import type { DataSource } from 'typeorm'; +import type { Provider } from '@nestjs/common'; + +const $usersRepository: Provider = { + provide: DI.usersRepository, + useFactory: (db: DataSource) => db.getRepository(User), + inject: [DI.db], +}; + +const $notesRepository: Provider = { + provide: DI.notesRepository, + useFactory: (db: DataSource) => db.getRepository(Note), + inject: [DI.db], +}; + +const $announcementsRepository: Provider = { + provide: DI.announcementsRepository, + useFactory: (db: DataSource) => db.getRepository(Announcement), + inject: [DI.db], +}; + +const $announcementReadsRepository: Provider = { + provide: DI.announcementReadsRepository, + useFactory: (db: DataSource) => db.getRepository(AnnouncementRead), + inject: [DI.db], +}; + +const $appsRepository: Provider = { + provide: DI.appsRepository, + useFactory: (db: DataSource) => db.getRepository(App), + inject: [DI.db], +}; + +const $noteFavoritesRepository: Provider = { + provide: DI.noteFavoritesRepository, + useFactory: (db: DataSource) => db.getRepository(NoteFavorite), + inject: [DI.db], +}; + +const $noteThreadMutingsRepository: Provider = { + provide: DI.noteThreadMutingsRepository, + useFactory: (db: DataSource) => db.getRepository(NoteThreadMuting), + inject: [DI.db], +}; + +const $noteReactionsRepository: Provider = { + provide: DI.noteReactionsRepository, + useFactory: (db: DataSource) => db.getRepository(NoteReaction), + inject: [DI.db], +}; + +const $noteUnreadsRepository: Provider = { + provide: DI.noteUnreadsRepository, + useFactory: (db: DataSource) => db.getRepository(NoteUnread), + inject: [DI.db], +}; + +const $pollsRepository: Provider = { + provide: DI.pollsRepository, + useFactory: (db: DataSource) => db.getRepository(Poll), + inject: [DI.db], +}; + +const $pollVotesRepository: Provider = { + provide: DI.pollVotesRepository, + useFactory: (db: DataSource) => db.getRepository(PollVote), + inject: [DI.db], +}; + +const $userProfilesRepository: Provider = { + provide: DI.userProfilesRepository, + useFactory: (db: DataSource) => db.getRepository(UserProfile), + inject: [DI.db], +}; + +const $userKeypairsRepository: Provider = { + provide: DI.userKeypairsRepository, + useFactory: (db: DataSource) => db.getRepository(UserKeypair), + inject: [DI.db], +}; + +const $userPendingsRepository: Provider = { + provide: DI.userPendingsRepository, + useFactory: (db: DataSource) => db.getRepository(UserPending), + inject: [DI.db], +}; + +const $attestationChallengesRepository: Provider = { + provide: DI.attestationChallengesRepository, + useFactory: (db: DataSource) => db.getRepository(AttestationChallenge), + inject: [DI.db], +}; + +const $userSecurityKeysRepository: Provider = { + provide: DI.userSecurityKeysRepository, + useFactory: (db: DataSource) => db.getRepository(UserSecurityKey), + inject: [DI.db], +}; + +const $userPublickeysRepository: Provider = { + provide: DI.userPublickeysRepository, + useFactory: (db: DataSource) => db.getRepository(UserPublickey), + inject: [DI.db], +}; + +const $userListsRepository: Provider = { + provide: DI.userListsRepository, + useFactory: (db: DataSource) => db.getRepository(UserList), + inject: [DI.db], +}; + +const $userListJoiningsRepository: Provider = { + provide: DI.userListJoiningsRepository, + useFactory: (db: DataSource) => db.getRepository(UserListJoining), + inject: [DI.db], +}; + +const $userGroupsRepository: Provider = { + provide: DI.userGroupsRepository, + useFactory: (db: DataSource) => db.getRepository(UserGroup), + inject: [DI.db], +}; + +const $userGroupJoiningsRepository: Provider = { + provide: DI.userGroupJoiningsRepository, + useFactory: (db: DataSource) => db.getRepository(UserGroupJoining), + inject: [DI.db], +}; + +const $userGroupInvitationsRepository: Provider = { + provide: DI.userGroupInvitationsRepository, + useFactory: (db: DataSource) => db.getRepository(UserGroupInvitation), + inject: [DI.db], +}; + +const $userNotePiningsRepository: Provider = { + provide: DI.userNotePiningsRepository, + useFactory: (db: DataSource) => db.getRepository(UserNotePining), + inject: [DI.db], +}; + +const $userIpsRepository: Provider = { + provide: DI.userIpsRepository, + useFactory: (db: DataSource) => db.getRepository(UserIp), + inject: [DI.db], +}; + +const $usedUsernamesRepository: Provider = { + provide: DI.usedUsernamesRepository, + useFactory: (db: DataSource) => db.getRepository(UsedUsername), + inject: [DI.db], +}; + +const $followingsRepository: Provider = { + provide: DI.followingsRepository, + useFactory: (db: DataSource) => db.getRepository(Following), + inject: [DI.db], +}; + +const $followRequestsRepository: Provider = { + provide: DI.followRequestsRepository, + useFactory: (db: DataSource) => db.getRepository(FollowRequest), + inject: [DI.db], +}; + +const $instancesRepository: Provider = { + provide: DI.instancesRepository, + useFactory: (db: DataSource) => db.getRepository(Instance), + inject: [DI.db], +}; + +const $emojisRepository: Provider = { + provide: DI.emojisRepository, + useFactory: (db: DataSource) => db.getRepository(Emoji), + inject: [DI.db], +}; + +const $driveFilesRepository: Provider = { + provide: DI.driveFilesRepository, + useFactory: (db: DataSource) => db.getRepository(DriveFile), + inject: [DI.db], +}; + +const $driveFoldersRepository: Provider = { + provide: DI.driveFoldersRepository, + useFactory: (db: DataSource) => db.getRepository(DriveFolder), + inject: [DI.db], +}; + +const $notificationsRepository: Provider = { + provide: DI.notificationsRepository, + useFactory: (db: DataSource) => db.getRepository(Notification), + inject: [DI.db], +}; + +const $metasRepository: Provider = { + provide: DI.metasRepository, + useFactory: (db: DataSource) => db.getRepository(Meta), + inject: [DI.db], +}; + +const $mutingsRepository: Provider = { + provide: DI.mutingsRepository, + useFactory: (db: DataSource) => db.getRepository(Muting), + inject: [DI.db], +}; + +const $blockingsRepository: Provider = { + provide: DI.blockingsRepository, + useFactory: (db: DataSource) => db.getRepository(Blocking), + inject: [DI.db], +}; + +const $swSubscriptionsRepository: Provider = { + provide: DI.swSubscriptionsRepository, + useFactory: (db: DataSource) => db.getRepository(SwSubscription), + inject: [DI.db], +}; + +const $hashtagsRepository: Provider = { + provide: DI.hashtagsRepository, + useFactory: (db: DataSource) => db.getRepository(Hashtag), + inject: [DI.db], +}; + +const $abuseUserReportsRepository: Provider = { + provide: DI.abuseUserReportsRepository, + useFactory: (db: DataSource) => db.getRepository(AbuseUserReport), + inject: [DI.db], +}; + +const $registrationTicketsRepository: Provider = { + provide: DI.registrationTicketsRepository, + useFactory: (db: DataSource) => db.getRepository(RegistrationTicket), + inject: [DI.db], +}; + +const $authSessionsRepository: Provider = { + provide: DI.authSessionsRepository, + useFactory: (db: DataSource) => db.getRepository(AuthSession), + inject: [DI.db], +}; + +const $accessTokensRepository: Provider = { + provide: DI.accessTokensRepository, + useFactory: (db: DataSource) => db.getRepository(AccessToken), + inject: [DI.db], +}; + +const $signinsRepository: Provider = { + provide: DI.signinsRepository, + useFactory: (db: DataSource) => db.getRepository(Signin), + inject: [DI.db], +}; + +const $messagingMessagesRepository: Provider = { + provide: DI.messagingMessagesRepository, + useFactory: (db: DataSource) => db.getRepository(MessagingMessage), + inject: [DI.db], +}; + +const $pagesRepository: Provider = { + provide: DI.pagesRepository, + useFactory: (db: DataSource) => db.getRepository(Page), + inject: [DI.db], +}; + +const $pageLikesRepository: Provider = { + provide: DI.pageLikesRepository, + useFactory: (db: DataSource) => db.getRepository(PageLike), + inject: [DI.db], +}; + +const $galleryPostsRepository: Provider = { + provide: DI.galleryPostsRepository, + useFactory: (db: DataSource) => db.getRepository(GalleryPost), + inject: [DI.db], +}; + +const $galleryLikesRepository: Provider = { + provide: DI.galleryLikesRepository, + useFactory: (db: DataSource) => db.getRepository(GalleryLike), + inject: [DI.db], +}; + +const $moderationLogsRepository: Provider = { + provide: DI.moderationLogsRepository, + useFactory: (db: DataSource) => db.getRepository(ModerationLog), + inject: [DI.db], +}; + +const $clipsRepository: Provider = { + provide: DI.clipsRepository, + useFactory: (db: DataSource) => db.getRepository(Clip), + inject: [DI.db], +}; + +const $clipNotesRepository: Provider = { + provide: DI.clipNotesRepository, + useFactory: (db: DataSource) => db.getRepository(ClipNote), + inject: [DI.db], +}; + +const $antennasRepository: Provider = { + provide: DI.antennasRepository, + useFactory: (db: DataSource) => db.getRepository(Antenna), + inject: [DI.db], +}; + +const $antennaNotesRepository: Provider = { + provide: DI.antennaNotesRepository, + useFactory: (db: DataSource) => db.getRepository(AntennaNote), + inject: [DI.db], +}; + +const $promoNotesRepository: Provider = { + provide: DI.promoNotesRepository, + useFactory: (db: DataSource) => db.getRepository(PromoNote), + inject: [DI.db], +}; + +const $promoReadsRepository: Provider = { + provide: DI.promoReadsRepository, + useFactory: (db: DataSource) => db.getRepository(PromoRead), + inject: [DI.db], +}; + +const $relaysRepository: Provider = { + provide: DI.relaysRepository, + useFactory: (db: DataSource) => db.getRepository(Relay), + inject: [DI.db], +}; + +const $mutedNotesRepository: Provider = { + provide: DI.mutedNotesRepository, + useFactory: (db: DataSource) => db.getRepository(MutedNote), + inject: [DI.db], +}; + +const $channelsRepository: Provider = { + provide: DI.channelsRepository, + useFactory: (db: DataSource) => db.getRepository(Channel), + inject: [DI.db], +}; + +const $channelFollowingsRepository: Provider = { + provide: DI.channelFollowingsRepository, + useFactory: (db: DataSource) => db.getRepository(ChannelFollowing), + inject: [DI.db], +}; + +const $channelNotePiningsRepository: Provider = { + provide: DI.channelNotePiningsRepository, + useFactory: (db: DataSource) => db.getRepository(ChannelNotePining), + inject: [DI.db], +}; + +const $registryItemsRepository: Provider = { + provide: DI.registryItemsRepository, + useFactory: (db: DataSource) => db.getRepository(RegistryItem), + inject: [DI.db], +}; + +const $webhooksRepository: Provider = { + provide: DI.webhooksRepository, + useFactory: (db: DataSource) => db.getRepository(Webhook), + inject: [DI.db], +}; + +const $adsRepository: Provider = { + provide: DI.adsRepository, + useFactory: (db: DataSource) => db.getRepository(Ad), + inject: [DI.db], +}; + +const $passwordResetRequestsRepository: Provider = { + provide: DI.passwordResetRequestsRepository, + useFactory: (db: DataSource) => db.getRepository(PasswordResetRequest), + inject: [DI.db], +}; + +@Module({ + imports: [ + ], + providers: [ + $usersRepository, + $notesRepository, + $announcementsRepository, + $announcementReadsRepository, + $appsRepository, + $noteFavoritesRepository, + $noteThreadMutingsRepository, + $noteReactionsRepository, + $noteUnreadsRepository, + $pollsRepository, + $pollVotesRepository, + $userProfilesRepository, + $userKeypairsRepository, + $userPendingsRepository, + $attestationChallengesRepository, + $userSecurityKeysRepository, + $userPublickeysRepository, + $userListsRepository, + $userListJoiningsRepository, + $userGroupsRepository, + $userGroupJoiningsRepository, + $userGroupInvitationsRepository, + $userNotePiningsRepository, + $userIpsRepository, + $usedUsernamesRepository, + $followingsRepository, + $followRequestsRepository, + $instancesRepository, + $emojisRepository, + $driveFilesRepository, + $driveFoldersRepository, + $notificationsRepository, + $metasRepository, + $mutingsRepository, + $blockingsRepository, + $swSubscriptionsRepository, + $hashtagsRepository, + $abuseUserReportsRepository, + $registrationTicketsRepository, + $authSessionsRepository, + $accessTokensRepository, + $signinsRepository, + $messagingMessagesRepository, + $pagesRepository, + $pageLikesRepository, + $galleryPostsRepository, + $galleryLikesRepository, + $moderationLogsRepository, + $clipsRepository, + $clipNotesRepository, + $antennasRepository, + $antennaNotesRepository, + $promoNotesRepository, + $promoReadsRepository, + $relaysRepository, + $mutedNotesRepository, + $channelsRepository, + $channelFollowingsRepository, + $channelNotePiningsRepository, + $registryItemsRepository, + $webhooksRepository, + $adsRepository, + $passwordResetRequestsRepository, + ], + exports: [ + $usersRepository, + $notesRepository, + $announcementsRepository, + $announcementReadsRepository, + $appsRepository, + $noteFavoritesRepository, + $noteThreadMutingsRepository, + $noteReactionsRepository, + $noteUnreadsRepository, + $pollsRepository, + $pollVotesRepository, + $userProfilesRepository, + $userKeypairsRepository, + $userPendingsRepository, + $attestationChallengesRepository, + $userSecurityKeysRepository, + $userPublickeysRepository, + $userListsRepository, + $userListJoiningsRepository, + $userGroupsRepository, + $userGroupJoiningsRepository, + $userGroupInvitationsRepository, + $userNotePiningsRepository, + $userIpsRepository, + $usedUsernamesRepository, + $followingsRepository, + $followRequestsRepository, + $instancesRepository, + $emojisRepository, + $driveFilesRepository, + $driveFoldersRepository, + $notificationsRepository, + $metasRepository, + $mutingsRepository, + $blockingsRepository, + $swSubscriptionsRepository, + $hashtagsRepository, + $abuseUserReportsRepository, + $registrationTicketsRepository, + $authSessionsRepository, + $accessTokensRepository, + $signinsRepository, + $messagingMessagesRepository, + $pagesRepository, + $pageLikesRepository, + $galleryPostsRepository, + $galleryLikesRepository, + $moderationLogsRepository, + $clipsRepository, + $clipNotesRepository, + $antennasRepository, + $antennaNotesRepository, + $promoNotesRepository, + $promoReadsRepository, + $relaysRepository, + $mutedNotesRepository, + $channelsRepository, + $channelFollowingsRepository, + $channelNotePiningsRepository, + $registryItemsRepository, + $webhooksRepository, + $adsRepository, + $passwordResetRequestsRepository, + ], +}) +export class RepositoryModule {} diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index c3d0592256..f67b09b1cc 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -2,7 +2,7 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import Xev from 'xev'; -import Logger from '@/services/logger.js'; +import Logger from '@/logger.js'; import { envOption } from '../env.js'; // for typeorm diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index bf51960482..ec09b4350b 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -6,14 +6,17 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; import semver from 'semver'; - -import Logger from '@/services/logger.js'; -import loadConfig from '@/config/load.js'; -import { Config } from '@/config/types.js'; -import { lessThan } from '@/prelude/array.js'; -import { envOption } from '../env.js'; +import { NestFactory } from '@nestjs/core'; +import Logger from '@/logger.js'; +import { loadConfig } from '@/config.js'; +import type { Config } from '@/config.js'; +import { lessThan } from '@/misc/prelude/array.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; -import { db, initDb } from '../db/postgre.js'; +import { DaemonModule } from '@/daemons/DaemonModule.js'; +import { JanitorService } from '@/daemons/JanitorService.js'; +import { QueueStatsService } from '@/daemons/QueueStatsService.js'; +import { ServerStatsService } from '@/daemons/ServerStatsService.js'; +import { envOption } from '../env.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -60,7 +63,7 @@ export async function masterMain() { await showMachineInfo(bootLogger); showNodejsVersion(); config = loadConfigBoot(); - await connectDb(); + //await connectDb(); } catch (e) { bootLogger.error('Fatal error occurred during initialization', null, true); process.exit(1); @@ -75,9 +78,11 @@ export async function masterMain() { bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); if (!envOption.noDaemons) { - import('../daemons/server-stats.js').then(x => x.default()); - import('../daemons/queue-stats.js').then(x => x.default()); - import('../daemons/janitor.js').then(x => x.default()); + const daemons = await NestFactory.createApplicationContext(DaemonModule); + daemons.enableShutdownHooks(); + daemons.get(JanitorService).start(); + daemons.get(QueueStatsService).start(); + daemons.get(ServerStatsService).start(); } } @@ -127,6 +132,7 @@ function loadConfigBoot(): Config { return config; } +/* async function connectDb(): Promise { const dbLogger = bootLogger.createSubLogger('db'); @@ -136,14 +142,15 @@ async function connectDb(): Promise { await initDb(); const v = await db.query('SHOW server_version').then(x => x[0].server_version); dbLogger.succ(`Connected: v${v}`); - } catch (e) { + } catch (err) { dbLogger.error('Cannot connect', null, true); - dbLogger.error(e); + dbLogger.error(err); process.exit(1); } } +*/ -async function spawnWorkers(limit: number = 1) { +async function spawnWorkers(limit = 1) { const workers = Math.min(limit, os.cpus().length); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); await Promise.all([...Array(workers)].map(spawnWorker)); @@ -155,7 +162,7 @@ function spawnWorker(): Promise { const worker = cluster.fork(); worker.on('message', message => { if (message === 'listenFailed') { - bootLogger.error(`The server Listen failed due to the previous error.`); + bootLogger.error('The server Listen failed due to the previous error.'); process.exit(1); } if (message !== 'ready') return; diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index 8038e25631..91f0c76317 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,17 +1,29 @@ import cluster from 'node:cluster'; -import { initDb } from '../db/postgre.js'; +import { NestFactory } from '@nestjs/core'; +import { envOption } from '@/env.js'; +import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; +import { ServerService } from '@/server/ServerService.js'; +import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; +import { AppModule } from '../AppModule.js'; /** * Init worker process */ export async function workerMain() { - await initDb(); + const app = await NestFactory.createApplicationContext(AppModule); + app.enableShutdownHooks(); // start server - await import('../server/index.js').then(x => x.default()); + const serverService = app.get(ServerService); + serverService.launch(); // start job queue - import('../queue/index.js').then(x => x.default()); + if (!envOption.onlyServer) { + const queueProcessorService = app.get(QueueProcessorService); + queueProcessorService.start(); + } + + app.get(ChartManagementService).run(); if (cluster.isWorker) { // Send a 'ready' message to parent process diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts new file mode 100644 index 0000000000..11d8db5c04 --- /dev/null +++ b/packages/backend/src/config.ts @@ -0,0 +1,149 @@ +/** + * Config loader + */ + +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as yaml from 'js-yaml'; + +/** + * ユーザーが設定する必要のある情報 + */ +export type Source = { + repository_url?: string; + feedback_url?: string; + url: string; + port: number; + disableHsts?: boolean; + db: { + host: string; + port: number; + db: string; + user: string; + pass: string; + disableCache?: boolean; + extra?: { [x: string]: string }; + }; + redis: { + host: string; + port: number; + family?: number; + pass: string; + db?: number; + prefix?: string; + }; + elasticsearch: { + host: string; + port: number; + ssl?: boolean; + user?: string; + pass?: string; + index?: string; + }; + + proxy?: string; + proxySmtp?: string; + proxyBypassHosts?: string[]; + + allowedPrivateNetworks?: string[]; + + maxFileSize?: number; + + accesslog?: string; + + clusterLimit?: number; + + id: string; + + outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; + + deliverJobConcurrency?: number; + inboxJobConcurrency?: number; + deliverJobPerSec?: number; + inboxJobPerSec?: number; + deliverJobMaxAttempts?: number; + inboxJobMaxAttempts?: number; + + syslog: { + host: string; + port: number; + }; + + mediaProxy?: string; + proxyRemoteFiles?: boolean; + + signToActivityPubGet?: boolean; +}; + +/** + * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 + */ +export type Mixin = { + version: string; + host: string; + hostname: string; + scheme: string; + wsScheme: string; + apiUrl: string; + wsUrl: string; + authUrl: string; + driveUrl: string; + userAgent: string; + clientEntry: string; +}; + +export type Config = Source & Mixin; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +/** + * Path of configuration directory + */ +const dir = `${_dirname}/../../../.config`; + +/** + * Path of configuration file + */ +const path = process.env.NODE_ENV === 'test' + ? `${dir}/test.yml` + : `${dir}/default.yml`; + +export function loadConfig() { + const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); + const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_client_dist_/manifest.json`, 'utf-8')); + const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; + + const mixin = {} as Mixin; + + const url = tryCreateUrl(config.url); + + config.url = url.origin; + + config.port = config.port ?? parseInt(process.env.PORT ?? '', 10); + + mixin.version = meta.version; + mixin.host = url.host; + mixin.hostname = url.hostname; + mixin.scheme = url.protocol.replace(/:$/, ''); + mixin.wsScheme = mixin.scheme.replace('http', 'ws'); + mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; + mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; + mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; + mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; + mixin.userAgent = `Misskey/${meta.version} (${config.url})`; + mixin.clientEntry = clientManifest['src/init.ts']; + + if (!config.redis.prefix) config.redis.prefix = mixin.host; + + return Object.assign(config, mixin); +} + +function tryCreateUrl(url: string) { + try { + return new URL(url); + } catch (e) { + throw `url="${url}" is not a valid URL.`; + } +} diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts deleted file mode 100644 index 3e53b00036..0000000000 --- a/packages/backend/src/config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import load from './load.js'; - -export default load(); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts deleted file mode 100644 index 9654a4f3b0..0000000000 --- a/packages/backend/src/config/load.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Config loader - */ - -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as yaml from 'js-yaml'; -import { Source, Mixin } from './types.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -/** - * Path of configuration directory - */ -const dir = `${_dirname}/../../../../.config`; - -/** - * Path of configuration file - */ -const path = process.env.NODE_ENV === 'test' - ? `${dir}/test.yml` - : `${dir}/default.yml`; - -export default function load() { - const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); - const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); - const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; - - const mixin = {} as Mixin; - - const url = tryCreateUrl(config.url); - - config.url = url.origin; - - config.port = config.port || parseInt(process.env.PORT || '', 10); - - mixin.version = meta.version; - mixin.host = url.host; - mixin.hostname = url.hostname; - mixin.scheme = url.protocol.replace(/:$/, ''); - mixin.wsScheme = mixin.scheme.replace('http', 'ws'); - mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; - mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; - mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; - mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; - mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts']; - - if (!config.redis.prefix) config.redis.prefix = mixin.host; - - return Object.assign(config, mixin); -} - -function tryCreateUrl(url: string) { - try { - return new URL(url); - } catch (e) { - throw `url="${url}" is not a valid URL.`; - } -} diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts deleted file mode 100644 index 78510c8377..0000000000 --- a/packages/backend/src/config/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * ユーザーが設定する必要のある情報 - */ -export type Source = { - repository_url?: string; - feedback_url?: string; - url: string; - port: number; - disableHsts?: boolean; - db: { - host: string; - port: number; - db: string; - user: string; - pass: string; - disableCache?: boolean; - extra?: { [x: string]: string }; - }; - redis: { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; - }; - elasticsearch: { - host: string; - port: number; - ssl?: boolean; - user?: string; - pass?: string; - index?: string; - }; - - proxy?: string; - proxySmtp?: string; - proxyBypassHosts?: string[]; - - allowedPrivateNetworks?: string[]; - - maxFileSize?: number; - - accesslog?: string; - - clusterLimit?: number; - - id: string; - - outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; - - deliverJobConcurrency?: number; - inboxJobConcurrency?: number; - deliverJobPerSec?: number; - inboxJobPerSec?: number; - deliverJobMaxAttempts?: number; - inboxJobMaxAttempts?: number; - - syslog: { - host: string; - port: number; - }; - - mediaProxy?: string; - proxyRemoteFiles?: boolean; - - signToActivityPubGet?: boolean; -}; - -/** - * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 - */ -export type Mixin = { - version: string; - host: string; - hostname: string; - scheme: string; - wsScheme: string; - apiUrl: string; - wsUrl: string; - authUrl: string; - driveUrl: string; - userAgent: string; - clientEntry: string; -}; - -export type Config = Source & Mixin; diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts new file mode 100644 index 0000000000..204e1d0170 --- /dev/null +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { User } from '@/models/entities/User.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { RelayService } from '@/core/RelayService.js'; +import { ApDeliverManagerService } from '@/core/remote/activitypub/ApDeliverManagerService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; + +@Injectable() +export class AccountUpdateService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, + private relayService: RelayService, + ) { + } + + public async publishToFollowers(userId: User['id']) { + const user = await this.usersRepository.findOneBy({ id: userId }); + if (user == null) throw new Error('user not found'); + + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 + if (this.userEntityService.isLocalUser(user)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); + } + } +} diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts new file mode 100644 index 0000000000..1cfc3382a9 --- /dev/null +++ b/packages/backend/src/core/AiService.ts @@ -0,0 +1,60 @@ +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { Inject, Injectable } from '@nestjs/common'; +import * as nsfw from 'nsfwjs'; +import si from 'systeminformation'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const REQUIRED_CPU_FLAGS = ['avx2', 'fma']; +let isSupportedCpu: undefined | boolean = undefined; + +@Injectable() +export class AiService { + #model: nsfw.NSFWJS; + + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + public async detectSensitive(path: string): Promise { + try { + if (isSupportedCpu === undefined) { + const cpuFlags = await this.#getCpuFlags(); + isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); + } + + if (!isSupportedCpu) { + console.error('This CPU cannot use TensorFlow.'); + return null; + } + + const tf = await import('@tensorflow/tfjs-node'); + + if (this.#model == null) this.#model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); + + const buffer = await fs.promises.readFile(path); + const image = await tf.node.decodeImage(buffer, 3) as any; + try { + const predictions = await this.#model.classify(image); + return predictions; + } finally { + image.dispose(); + } + } catch (err) { + console.error(err); + return null; + } + } + + 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 new file mode 100644 index 0000000000..8993880a06 --- /dev/null +++ b/packages/backend/src/core/AntennaService.ts @@ -0,0 +1,228 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import * as Acct from '@/misc/acct.js'; +import { Cache } from '@/misc/cache.js'; +import type { Packed } from '@/misc/schema.js'; +import { DI } from '@/di-symbols.js'; +import { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import { UtilityService } from './UtilityService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class AntennaService implements OnApplicationShutdown { + #antennasFetched: boolean; + #antennas: Antenna[]; + #blockingCache: Cache; + + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private utilityService: UtilityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + ) { + this.#antennasFetched = false; + this.#antennas = []; + this.#blockingCache = new Cache(1000 * 60 * 5); + + this.redisSubscriber.on('message', this.onRedisMessage); + } + + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onRedisMessage); + } + + private async onRedisMessage(_, data) { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message; + switch (type) { + case 'antennaCreated': + this.#antennas.push(body); + break; + case 'antennaUpdated': + this.#antennas[this.#antennas.findIndex(a => a.id === body.id)] = body; + break; + case 'antennaDeleted': + this.#antennas = this.#antennas.filter(a => a.id !== body.id); + break; + default: + break; + } + } + } + + public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise { + // 通知しない設定になっているか、自分自身の投稿なら既読にする + const read = !antenna.notify || (antenna.userId === noteUser.id); + + this.antennaNotesRepository.insert({ + id: this.idService.genId(), + antennaId: antenna.id, + noteId: note.id, + read: read, + }); + + this.globalEventServie.publishAntennaStream(antenna.id, 'note', note); + + if (!read) { + const mutings = await this.mutingsRepository.find({ + where: { + muterId: antenna.userId, + }, + select: ['muteeId'], + }); + + // Copy + const _note: Note = { + ...note, + }; + + if (note.replyId != null) { + _note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId }); + } + if (note.renoteId != null) { + _note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); + } + + if (isUserRelated(_note, new Set(mutings.map(x => x.muteeId)))) { + return; + } + + // 2秒経っても既読にならなかったら通知 + setTimeout(async () => { + const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); + if (unread) { + this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); + } + }, 2000); + } + } + + // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている + + /** + * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい + */ + 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; + + // アンテナ作成者がノート作成者にブロックされていたらスキップ + const blockings = await this.#blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); + if (blockings.some(blocking => blocking === antenna.userId)) return false; + + if (note.visibility === 'followers') { + if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; + if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; + } + + if (!antenna.withReplies && note.replyId != null) return false; + + if (antenna.src === 'home') { + if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; + if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; + } else if (antenna.src === 'list') { + const listUsers = (await this.userListJoiningsRepository.findBy({ + userListId: antenna.userListId!, + })).map(x => x.userId); + + if (!listUsers.includes(note.userId)) return false; + } else if (antenna.src === 'group') { + const joining = await this.userGroupJoiningsRepository.findOneByOrFail({ id: antenna.userGroupJoiningId! }); + + const groupUsers = (await this.userGroupJoiningsRepository.findBy({ + userGroupId: joining.userGroupId, + })).map(x => x.userId); + + if (!groupUsers.includes(note.userId)) return false; + } else if (antenna.src === 'users') { + const accts = antenna.users.map(x => { + const { username, host } = Acct.parse(x); + return this.utilityService.getFullApAccount(username, host).toLowerCase(); + }); + if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; + } + + const keywords = antenna.keywords + // Clean up + .map(xs => xs.filter(x => x !== '')) + .filter(xs => xs.length > 0); + + if (keywords.length > 0) { + if (note.text == null) return false; + + const matched = keywords.some(and => + and.every(keyword => + antenna.caseSensitive + ? note.text!.includes(keyword) + : note.text!.toLowerCase().includes(keyword.toLowerCase()), + )); + + if (!matched) return false; + } + + const excludeKeywords = antenna.excludeKeywords + // Clean up + .map(xs => xs.filter(x => x !== '')) + .filter(xs => xs.length > 0); + + if (excludeKeywords.length > 0) { + if (note.text == null) return false; + + const matched = excludeKeywords.some(and => + and.every(keyword => + antenna.caseSensitive + ? note.text!.includes(keyword) + : note.text!.toLowerCase().includes(keyword.toLowerCase()), + )); + + if (matched) return false; + } + + if (antenna.withFile) { + if (note.fileIds && note.fileIds.length === 0) return false; + } + + // TODO: eval expression + + return true; + } + + public async getAntennas() { + if (!this.#antennasFetched) { + this.#antennas = await this.antennasRepository.find(); + this.#antennasFetched = true; + } + + return this.#antennas; + } +} diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts new file mode 100644 index 0000000000..f3c345493b --- /dev/null +++ b/packages/backend/src/core/AppLockService.ts @@ -0,0 +1,40 @@ +import { promisify } from 'node:util'; +import { Inject, Injectable } from '@nestjs/common'; +import redisLock from 'redis-lock'; +import Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; + +/** + * Retry delay (ms) for lock acquisition + */ +const retryDelay = 100; + +@Injectable() +export class AppLockService { + #lock: (key: string, timeout?: number) => Promise<() => void>; + + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + ) { + this.#lock = promisify(redisLock(this.redisClient, retryDelay)); + } + + /** + * Get AP Object lock + * @param uri AP object ID + * @param timeout Lock timeout (ms), The timeout releases previous lock. + * @returns Unlock function + */ + public getApLock(uri: string, timeout = 30 * 1000): Promise<() => void> { + return this.#lock(`ap-object:${uri}`, timeout); + } + + public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> { + return this.#lock(`instance:${host}`, timeout); + } + + 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 new file mode 100644 index 0000000000..891e5315c2 --- /dev/null +++ b/packages/backend/src/core/CaptchaService.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { HttpRequestService } from './HttpRequestService.js'; + +type CaptchaResponse = { + success: boolean; + 'error-codes'?: string[]; +}; + +@Injectable() +export class CaptchaService { + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + } + + async #getCaptchaResponse(url: string, secret: string, response: string): Promise { + const params = new URLSearchParams({ + secret, + response, + }); + + const res = await fetch(url, { + method: 'POST', + body: params, + headers: { + 'User-Agent': this.config.userAgent, + }, + // TODO + //timeout: 10 * 1000, + agent: (url, bypassProxy) => this.httpRequestService.getAgentByUrl(url, bypassProxy), + }).catch(err => { + throw `${err.message ?? err}`; + }); + + if (!res.ok) { + throw `${res.status}`; + } + + return await res.json() as CaptchaResponse; + } + + public async verifyRecaptcha(secret: string, response: string): Promise { + const result = await this.#getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => { + throw `recaptcha-request-failed: ${e}`; + }); + + if (result.success !== true) { + const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; + throw `recaptcha-failed: ${errorCodes}`; + } + } + + public async verifyHcaptcha(secret: string, response: string): Promise { + const result = await this.#getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => { + throw `hcaptcha-request-failed: ${e}`; + }); + + if (result.success !== true) { + const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; + throw `hcaptcha-failed: ${errorCodes}`; + } + } +} + diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts new file mode 100644 index 0000000000..f6dd0debb9 --- /dev/null +++ b/packages/backend/src/core/CoreModule.ts @@ -0,0 +1,696 @@ +import { Module } from '@nestjs/common'; +import { DI } from '../di-symbols.js'; +import { AccountUpdateService } from './AccountUpdateService.js'; +import { AiService } from './AiService.js'; +import { AntennaService } from './AntennaService.js'; +import { AppLockService } from './AppLockService.js'; +import { CaptchaService } from './CaptchaService.js'; +import { CreateNotificationService } from './CreateNotificationService.js'; +import { CreateSystemUserService } from './CreateSystemUserService.js'; +import { CustomEmojiService } from './CustomEmojiService.js'; +import { DeleteAccountService } from './DeleteAccountService.js'; +import { DownloadService } from './DownloadService.js'; +import { DriveService } from './DriveService.js'; +import { EmailService } from './EmailService.js'; +import { FederatedInstanceService } from './FederatedInstanceService.js'; +import { FetchInstanceMetadataService } from './FetchInstanceMetadataService.js'; +import { GlobalEventService } from './GlobalEventService.js'; +import { HashtagService } from './HashtagService.js'; +import { HttpRequestService } from './HttpRequestService.js'; +import { IdService } from './IdService.js'; +import { ImageProcessingService } from './ImageProcessingService.js'; +import { InstanceActorService } from './InstanceActorService.js'; +import { InternalStorageService } from './InternalStorageService.js'; +import { MessagingService } from './MessagingService.js'; +import { MetaService } from './MetaService.js'; +import { MfmService } from './MfmService.js'; +import { ModerationLogService } from './ModerationLogService.js'; +import { NoteCreateService } from './NoteCreateService.js'; +import { NoteDeleteService } from './NoteDeleteService.js'; +import { NotePiningService } from './NotePiningService.js'; +import { NoteReadService } from './NoteReadService.js'; +import { NotificationService } from './NotificationService.js'; +import { PollService } from './PollService.js'; +import { PushNotificationService } from './PushNotificationService.js'; +import { QueryService } from './QueryService.js'; +import { ReactionService } from './ReactionService.js'; +import { RelayService } from './RelayService.js'; +import { S3Service } from './S3Service.js'; +import { SignupService } from './SignupService.js'; +import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; +import { UserBlockingService } from './UserBlockingService.js'; +import { UserCacheService } from './UserCacheService.js'; +import { UserFollowingService } from './UserFollowingService.js'; +import { UserKeypairStoreService } from './UserKeypairStoreService.js'; +import { UserListService } from './UserListService.js'; +import { UserMutingService } from './UserMutingService.js'; +import { UserSuspendService } from './UserSuspendService.js'; +import { VideoProcessingService } from './VideoProcessingService.js'; +import { WebhookService } from './WebhookService.js'; +import { ProxyAccountService } from './ProxyAccountService.js'; +import { UtilityService } from './UtilityService.js'; +import { FileInfoService } from './FileInfoService.js'; +import FederationChart from './chart/charts/federation.js'; +import NotesChart from './chart/charts/notes.js'; +import UsersChart from './chart/charts/users.js'; +import ActiveUsersChart from './chart/charts/active-users.js'; +import InstanceChart from './chart/charts/instance.js'; +import PerUserNotesChart from './chart/charts/per-user-notes.js'; +import DriveChart from './chart/charts/drive.js'; +import PerUserReactionsChart from './chart/charts/per-user-reactions.js'; +import HashtagChart from './chart/charts/hashtag.js'; +import PerUserFollowingChart from './chart/charts/per-user-following.js'; +import PerUserDriveChart from './chart/charts/per-user-drive.js'; +import ApRequestChart from './chart/charts/ap-request.js'; +import { ChartManagementService } from './chart/ChartManagementService.js'; +import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js'; +import { AntennaEntityService } from './entities/AntennaEntityService.js'; +import { AppEntityService } from './entities/AppEntityService.js'; +import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js'; +import { BlockingEntityService } from './entities/BlockingEntityService.js'; +import { ChannelEntityService } from './entities/ChannelEntityService.js'; +import { ClipEntityService } from './entities/ClipEntityService.js'; +import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; +import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; +import { EmojiEntityService } from './entities/EmojiEntityService.js'; +import { FollowingEntityService } from './entities/FollowingEntityService.js'; +import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js'; +import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js'; +import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js'; +import { HashtagEntityService } from './entities/HashtagEntityService.js'; +import { InstanceEntityService } from './entities/InstanceEntityService.js'; +import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js'; +import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; +import { MutingEntityService } from './entities/MutingEntityService.js'; +import { NoteEntityService } from './entities/NoteEntityService.js'; +import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js'; +import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js'; +import { NotificationEntityService } from './entities/NotificationEntityService.js'; +import { PageEntityService } from './entities/PageEntityService.js'; +import { PageLikeEntityService } from './entities/PageLikeEntityService.js'; +import { SigninEntityService } from './entities/SigninEntityService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { UserGroupEntityService } from './entities/UserGroupEntityService.js'; +import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js'; +import { UserListEntityService } from './entities/UserListEntityService.js'; +import { ApAudienceService } from './remote/activitypub/ApAudienceService.js'; +import { ApDbResolverService } from './remote/activitypub/ApDbResolverService.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; +import { ApInboxService } from './remote/activitypub/ApInboxService.js'; +import { ApLoggerService } from './remote/activitypub/ApLoggerService.js'; +import { ApMfmService } from './remote/activitypub/ApMfmService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { ApRequestService } from './remote/activitypub/ApRequestService.js'; +import { ApResolverService } from './remote/activitypub/ApResolverService.js'; +import { LdSignatureService } from './remote/activitypub/LdSignatureService.js'; +import { RemoteLoggerService } from './remote/RemoteLoggerService.js'; +import { ResolveUserService } from './remote/ResolveUserService.js'; +import { WebfingerService } from './remote/WebfingerService.js'; +import { ApImageService } from './remote/activitypub/models/ApImageService.js'; +import { ApMentionService } from './remote/activitypub/models/ApMentionService.js'; +import { ApNoteService } from './remote/activitypub/models/ApNoteService.js'; +import { ApPersonService } from './remote/activitypub/models/ApPersonService.js'; +import { ApQuestionService } from './remote/activitypub/models/ApQuestionService.js'; +import { QueueModule } from './queue/QueueModule.js'; +import { QueueService } from './QueueService.js'; +import type { Provider } from '@nestjs/common'; + +//#region 文字列ベースでのinjection用(循環参照対応のため) +const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useClass: AccountUpdateService }; +const $AiService: Provider = { provide: 'AiService', useClass: AiService }; +const $AntennaService: Provider = { provide: 'AntennaService', useClass: AntennaService }; +const $AppLockService: Provider = { provide: 'AppLockService', useClass: AppLockService }; +const $CaptchaService: Provider = { provide: 'CaptchaService', useClass: CaptchaService }; +const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useClass: CreateNotificationService }; +const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useClass: CreateSystemUserService }; +const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useClass: CustomEmojiService }; +const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useClass: DeleteAccountService }; +const $DownloadService: Provider = { provide: 'DownloadService', useClass: DownloadService }; +const $DriveService: Provider = { provide: 'DriveService', useClass: DriveService }; +const $EmailService: Provider = { provide: 'EmailService', useClass: EmailService }; +const $FederatedInstanceService: Provider = { provide: 'FederatedInstanceService', useClass: FederatedInstanceService }; +const $FetchInstanceMetadataService: Provider = { provide: 'FetchInstanceMetadataService', useClass: FetchInstanceMetadataService }; +const $GlobalEventService: Provider = { provide: 'GlobalEventService', useClass: GlobalEventService }; +const $HashtagService: Provider = { provide: 'HashtagService', useClass: HashtagService }; +const $HttpRequestService: Provider = { provide: 'HttpRequestService', useClass: HttpRequestService }; +const $IdService: Provider = { provide: 'IdService', useClass: IdService }; +const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useClass: ImageProcessingService }; +const $InstanceActorService: Provider = { provide: 'InstanceActorService', useClass: InstanceActorService }; +const $InternalStorageService: Provider = { provide: 'InternalStorageService', useClass: InternalStorageService }; +const $MessagingService: Provider = { provide: 'MessagingService', useClass: MessagingService }; +const $MetaService: Provider = { provide: 'MetaService', useClass: MetaService }; +const $MfmService: Provider = { provide: 'MfmService', useClass: MfmService }; +const $ModerationLogService: Provider = { provide: 'ModerationLogService', useClass: ModerationLogService }; +const $NoteCreateService: Provider = { provide: 'NoteCreateService', useClass: NoteCreateService }; +const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useClass: NoteDeleteService }; +const $NotePiningService: Provider = { provide: 'NotePiningService', useClass: NotePiningService }; +const $NoteReadService: Provider = { provide: 'NoteReadService', useClass: NoteReadService }; +const $NotificationService: Provider = { provide: 'NotificationService', useClass: NotificationService }; +const $PollService: Provider = { provide: 'PollService', useClass: PollService }; +const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useClass: ProxyAccountService }; +const $PushNotificationService: Provider = { provide: 'PushNotificationService', useClass: PushNotificationService }; +const $QueryService: Provider = { provide: 'QueryService', useClass: QueryService }; +const $ReactionService: Provider = { provide: 'ReactionService', useClass: ReactionService }; +const $RelayService: Provider = { provide: 'RelayService', useClass: RelayService }; +const $S3Service: Provider = { provide: 'S3Service', useClass: S3Service }; +const $SignupService: Provider = { provide: 'SignupService', useClass: SignupService }; +const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useClass: TwoFactorAuthenticationService }; +const $UserBlockingService: Provider = { provide: 'UserBlockingService', useClass: UserBlockingService }; +const $UserCacheService: Provider = { provide: 'UserCacheService', useClass: UserCacheService }; +const $UserFollowingService: Provider = { provide: 'UserFollowingService', useClass: UserFollowingService }; +const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useClass: UserKeypairStoreService }; +const $UserListService: Provider = { provide: 'UserListService', useClass: UserListService }; +const $UserMutingService: Provider = { provide: 'UserMutingService', useClass: UserMutingService }; +const $UserSuspendService: Provider = { provide: 'UserSuspendService', useClass: UserSuspendService }; +const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useClass: VideoProcessingService }; +const $WebhookService: Provider = { provide: 'WebhookService', useClass: WebhookService }; +const $UtilityService: Provider = { provide: 'UtilityService', useClass: UtilityService }; +const $FileInfoService: Provider = { provide: 'FileInfoService', useClass: FileInfoService }; +const $FederationChart: Provider = { provide: 'FederationChart', useClass: FederationChart }; +const $NotesChart: Provider = { provide: 'NotesChart', useClass: NotesChart }; +const $UsersChart: Provider = { provide: 'UsersChart', useClass: UsersChart }; +const $ActiveUsersChart: Provider = { provide: 'ActiveUsersChart', useClass: ActiveUsersChart }; +const $InstanceChart: Provider = { provide: 'InstanceChart', useClass: InstanceChart }; +const $PerUserNotesChart: Provider = { provide: 'PerUserNotesChart', useClass: PerUserNotesChart }; +const $DriveChart: Provider = { provide: 'DriveChart', useClass: DriveChart }; +const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useClass: PerUserReactionsChart }; +const $HashtagChart: Provider = { provide: 'HashtagChart', useClass: HashtagChart }; +const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useClass: PerUserFollowingChart }; +const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useClass: PerUserDriveChart }; +const $ApRequestChart: Provider = { provide: 'ApRequestChart', useClass: ApRequestChart }; +const $ChartManagementService: Provider = { provide: 'ChartManagementService', useClass: ChartManagementService }; + +const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useClass: AbuseUserReportEntityService }; +const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useClass: AntennaEntityService }; +const $AppEntityService: Provider = { provide: 'AppEntityService', useClass: AppEntityService }; +const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useClass: AuthSessionEntityService }; +const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useClass: BlockingEntityService }; +const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useClass: ChannelEntityService }; +const $ClipEntityService: Provider = { provide: 'ClipEntityService', useClass: ClipEntityService }; +const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useClass: DriveFileEntityService }; +const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useClass: DriveFolderEntityService }; +const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useClass: EmojiEntityService }; +const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useClass: FollowingEntityService }; +const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useClass: FollowRequestEntityService }; +const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useClass: GalleryLikeEntityService }; +const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useClass: GalleryPostEntityService }; +const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useClass: HashtagEntityService }; +const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useClass: InstanceEntityService }; +const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useClass: MessagingMessageEntityService }; +const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useClass: ModerationLogEntityService }; +const $MutingEntityService: Provider = { provide: 'MutingEntityService', useClass: MutingEntityService }; +const $NoteEntityService: Provider = { provide: 'NoteEntityService', useClass: NoteEntityService }; +const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useClass: NoteFavoriteEntityService }; +const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useClass: NoteReactionEntityService }; +const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useClass: NotificationEntityService }; +const $PageEntityService: Provider = { provide: 'PageEntityService', useClass: PageEntityService }; +const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useClass: PageLikeEntityService }; +const $SigninEntityService: Provider = { provide: 'SigninEntityService', useClass: SigninEntityService }; +const $UserEntityService: Provider = { provide: 'UserEntityService', useClass: UserEntityService }; +const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useClass: UserGroupEntityService }; +const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useClass: UserGroupInvitationEntityService }; +const $UserListEntityService: Provider = { provide: 'UserListEntityService', useClass: UserListEntityService }; + +const $ApAudienceService: Provider = { provide: 'ApAudienceService', useClass: ApAudienceService }; +const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useClass: ApDbResolverService }; +const $ApDeliverManagerService: Provider = { provide: 'ApDeliverManagerService', useClass: ApDeliverManagerService }; +const $ApInboxService: Provider = { provide: 'ApInboxService', useClass: ApInboxService }; +const $ApLoggerService: Provider = { provide: 'ApLoggerService', useClass: ApLoggerService }; +const $ApMfmService: Provider = { provide: 'ApMfmService', useClass: ApMfmService }; +const $ApRendererService: Provider = { provide: 'ApRendererService', useClass: ApRendererService }; +const $ApRequestService: Provider = { provide: 'ApRequestService', useClass: ApRequestService }; +const $ApResolverService: Provider = { provide: 'ApResolverService', useClass: ApResolverService }; +const $LdSignatureService: Provider = { provide: 'LdSignatureService', useClass: LdSignatureService }; +const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useClass: RemoteLoggerService }; +const $ResolveUserService: Provider = { provide: 'ResolveUserService', useClass: ResolveUserService }; +const $WebfingerService: Provider = { provide: 'WebfingerService', useClass: WebfingerService }; +const $ApImageService: Provider = { provide: 'ApImageService', useClass: ApImageService }; +const $ApMentionService: Provider = { provide: 'ApMentionService', useClass: ApMentionService }; +const $ApNoteService: Provider = { provide: 'ApNoteService', useClass: ApNoteService }; +const $ApPersonService: Provider = { provide: 'ApPersonService', useClass: ApPersonService }; +const $ApQuestionService: Provider = { provide: 'ApQuestionService', useClass: ApQuestionService }; +//#endregion + +@Module({ + imports: [ + QueueModule, + ], + providers: [ + AccountUpdateService, + AiService, + AntennaService, + AppLockService, + CaptchaService, + CreateNotificationService, + CreateSystemUserService, + CustomEmojiService, + DeleteAccountService, + DownloadService, + DriveService, + EmailService, + FederatedInstanceService, + FetchInstanceMetadataService, + GlobalEventService, + HashtagService, + HttpRequestService, + IdService, + ImageProcessingService, + InstanceActorService, + InternalStorageService, + MessagingService, + MetaService, + MfmService, + ModerationLogService, + NoteCreateService, + NoteDeleteService, + NotePiningService, + NoteReadService, + NotificationService, + PollService, + ProxyAccountService, + PushNotificationService, + QueryService, + ReactionService, + RelayService, + S3Service, + SignupService, + TwoFactorAuthenticationService, + UserBlockingService, + UserCacheService, + UserFollowingService, + UserKeypairStoreService, + UserListService, + UserMutingService, + UserSuspendService, + VideoProcessingService, + WebhookService, + UtilityService, + FileInfoService, + FederationChart, + NotesChart, + UsersChart, + ActiveUsersChart, + InstanceChart, + PerUserNotesChart, + DriveChart, + PerUserReactionsChart, + HashtagChart, + PerUserFollowingChart, + PerUserDriveChart, + ApRequestChart, + ChartManagementService, + AbuseUserReportEntityService, + AntennaEntityService, + AppEntityService, + AuthSessionEntityService, + BlockingEntityService, + ChannelEntityService, + ClipEntityService, + DriveFileEntityService, + DriveFolderEntityService, + EmojiEntityService, + FollowingEntityService, + FollowRequestEntityService, + GalleryLikeEntityService, + GalleryPostEntityService, + HashtagEntityService, + InstanceEntityService, + MessagingMessageEntityService, + ModerationLogEntityService, + MutingEntityService, + NoteEntityService, + NoteFavoriteEntityService, + NoteReactionEntityService, + NotificationEntityService, + PageEntityService, + PageLikeEntityService, + SigninEntityService, + UserEntityService, + UserGroupEntityService, + UserGroupInvitationEntityService, + UserListEntityService, + ApAudienceService, + ApDbResolverService, + ApDeliverManagerService, + ApInboxService, + ApLoggerService, + ApMfmService, + ApRendererService, + ApRequestService, + ApResolverService, + LdSignatureService, + RemoteLoggerService, + ResolveUserService, + WebfingerService, + ApImageService, + ApMentionService, + ApNoteService, + ApPersonService, + ApQuestionService, + QueueService, + + //#region 文字列ベースでのinjection用(循環参照対応のため) + $AccountUpdateService, + $AiService, + $AntennaService, + $AppLockService, + $CaptchaService, + $CreateNotificationService, + $CreateSystemUserService, + $CustomEmojiService, + $DeleteAccountService, + $DownloadService, + $DriveService, + $EmailService, + $FederatedInstanceService, + $FetchInstanceMetadataService, + $GlobalEventService, + $HashtagService, + $HttpRequestService, + $IdService, + $ImageProcessingService, + $InstanceActorService, + $InternalStorageService, + $MessagingService, + $MetaService, + $MfmService, + $ModerationLogService, + $NoteCreateService, + $NoteDeleteService, + $NotePiningService, + $NoteReadService, + $NotificationService, + $PollService, + $ProxyAccountService, + $PushNotificationService, + $QueryService, + $ReactionService, + $RelayService, + $S3Service, + $SignupService, + $TwoFactorAuthenticationService, + $UserBlockingService, + $UserCacheService, + $UserFollowingService, + $UserKeypairStoreService, + $UserListService, + $UserMutingService, + $UserSuspendService, + $VideoProcessingService, + $WebhookService, + $UtilityService, + $FileInfoService, + $FederationChart, + $NotesChart, + $UsersChart, + $ActiveUsersChart, + $InstanceChart, + $PerUserNotesChart, + $DriveChart, + $PerUserReactionsChart, + $HashtagChart, + $PerUserFollowingChart, + $PerUserDriveChart, + $ApRequestChart, + $ChartManagementService, + $AbuseUserReportEntityService, + $AntennaEntityService, + $AppEntityService, + $AuthSessionEntityService, + $BlockingEntityService, + $ChannelEntityService, + $ClipEntityService, + $DriveFileEntityService, + $DriveFolderEntityService, + $EmojiEntityService, + $FollowingEntityService, + $FollowRequestEntityService, + $GalleryLikeEntityService, + $GalleryPostEntityService, + $HashtagEntityService, + $InstanceEntityService, + $MessagingMessageEntityService, + $ModerationLogEntityService, + $MutingEntityService, + $NoteEntityService, + $NoteFavoriteEntityService, + $NoteReactionEntityService, + $NotificationEntityService, + $PageEntityService, + $PageLikeEntityService, + $SigninEntityService, + $UserEntityService, + $UserGroupEntityService, + $UserGroupInvitationEntityService, + $UserListEntityService, + $ApAudienceService, + $ApDbResolverService, + $ApDeliverManagerService, + $ApInboxService, + $ApLoggerService, + $ApMfmService, + $ApRendererService, + $ApRequestService, + $ApResolverService, + $LdSignatureService, + $RemoteLoggerService, + $ResolveUserService, + $WebfingerService, + $ApImageService, + $ApMentionService, + $ApNoteService, + $ApPersonService, + $ApQuestionService, + //#endregion + ], + exports: [ + QueueModule, + AccountUpdateService, + AiService, + AntennaService, + AppLockService, + CaptchaService, + CreateNotificationService, + CreateSystemUserService, + CustomEmojiService, + DeleteAccountService, + DownloadService, + DriveService, + EmailService, + FederatedInstanceService, + FetchInstanceMetadataService, + GlobalEventService, + HashtagService, + HttpRequestService, + IdService, + ImageProcessingService, + InstanceActorService, + InternalStorageService, + MessagingService, + MetaService, + MfmService, + ModerationLogService, + NoteCreateService, + NoteDeleteService, + NotePiningService, + NoteReadService, + NotificationService, + PollService, + ProxyAccountService, + PushNotificationService, + QueryService, + ReactionService, + RelayService, + S3Service, + SignupService, + TwoFactorAuthenticationService, + UserBlockingService, + UserCacheService, + UserFollowingService, + UserKeypairStoreService, + UserListService, + UserMutingService, + UserSuspendService, + VideoProcessingService, + WebhookService, + UtilityService, + FileInfoService, + FederationChart, + NotesChart, + UsersChart, + ActiveUsersChart, + InstanceChart, + PerUserNotesChart, + DriveChart, + PerUserReactionsChart, + HashtagChart, + PerUserFollowingChart, + PerUserDriveChart, + ApRequestChart, + ChartManagementService, + AbuseUserReportEntityService, + AntennaEntityService, + AppEntityService, + AuthSessionEntityService, + BlockingEntityService, + ChannelEntityService, + ClipEntityService, + DriveFileEntityService, + DriveFolderEntityService, + EmojiEntityService, + FollowingEntityService, + FollowRequestEntityService, + GalleryLikeEntityService, + GalleryPostEntityService, + HashtagEntityService, + InstanceEntityService, + MessagingMessageEntityService, + ModerationLogEntityService, + MutingEntityService, + NoteEntityService, + NoteFavoriteEntityService, + NoteReactionEntityService, + NotificationEntityService, + PageEntityService, + PageLikeEntityService, + SigninEntityService, + UserEntityService, + UserGroupEntityService, + UserGroupInvitationEntityService, + UserListEntityService, + ApAudienceService, + ApDbResolverService, + ApDeliverManagerService, + ApInboxService, + ApLoggerService, + ApMfmService, + ApRendererService, + ApRequestService, + ApResolverService, + LdSignatureService, + RemoteLoggerService, + ResolveUserService, + WebfingerService, + ApImageService, + ApMentionService, + ApNoteService, + ApPersonService, + ApQuestionService, + QueueService, + + //#region 文字列ベースでのinjection用(循環参照対応のため) + $AccountUpdateService, + $AiService, + $AntennaService, + $AppLockService, + $CaptchaService, + $CreateNotificationService, + $CreateSystemUserService, + $CustomEmojiService, + $DeleteAccountService, + $DownloadService, + $DriveService, + $EmailService, + $FederatedInstanceService, + $FetchInstanceMetadataService, + $GlobalEventService, + $HashtagService, + $HttpRequestService, + $IdService, + $ImageProcessingService, + $InstanceActorService, + $InternalStorageService, + $MessagingService, + $MetaService, + $MfmService, + $ModerationLogService, + $NoteCreateService, + $NoteDeleteService, + $NotePiningService, + $NoteReadService, + $NotificationService, + $PollService, + $ProxyAccountService, + $PushNotificationService, + $QueryService, + $ReactionService, + $RelayService, + $S3Service, + $SignupService, + $TwoFactorAuthenticationService, + $UserBlockingService, + $UserCacheService, + $UserFollowingService, + $UserKeypairStoreService, + $UserListService, + $UserMutingService, + $UserSuspendService, + $VideoProcessingService, + $WebhookService, + $UtilityService, + $FileInfoService, + $FederationChart, + $NotesChart, + $UsersChart, + $ActiveUsersChart, + $InstanceChart, + $PerUserNotesChart, + $DriveChart, + $PerUserReactionsChart, + $HashtagChart, + $PerUserFollowingChart, + $PerUserDriveChart, + $ApRequestChart, + $ChartManagementService, + $AbuseUserReportEntityService, + $AntennaEntityService, + $AppEntityService, + $AuthSessionEntityService, + $BlockingEntityService, + $ChannelEntityService, + $ClipEntityService, + $DriveFileEntityService, + $DriveFolderEntityService, + $EmojiEntityService, + $FollowingEntityService, + $FollowRequestEntityService, + $GalleryLikeEntityService, + $GalleryPostEntityService, + $HashtagEntityService, + $InstanceEntityService, + $MessagingMessageEntityService, + $ModerationLogEntityService, + $MutingEntityService, + $NoteEntityService, + $NoteFavoriteEntityService, + $NoteReactionEntityService, + $NotificationEntityService, + $PageEntityService, + $PageLikeEntityService, + $SigninEntityService, + $UserEntityService, + $UserGroupEntityService, + $UserGroupInvitationEntityService, + $UserListEntityService, + $ApAudienceService, + $ApDbResolverService, + $ApDeliverManagerService, + $ApInboxService, + $ApLoggerService, + $ApMfmService, + $ApRendererService, + $ApRequestService, + $ApResolverService, + $LdSignatureService, + $RemoteLoggerService, + $ResolveUserService, + $WebfingerService, + $ApImageService, + $ApMentionService, + $ApNoteService, + $ApPersonService, + $ApQuestionService, + //#endregion + ], +}) +export class CoreModule {} diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts new file mode 100644 index 0000000000..10aa2c5df5 --- /dev/null +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -0,0 +1,114 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { Notification } from '@/models/entities/Notification.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { NotificationEntityService } from './entities/NotificationEntityService.js'; +import { PushNotificationService } from './PushNotificationService.js'; + +@Injectable() +export class CreateNotificationService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private notificationEntityService: NotificationEntityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + private pushNotificationService: PushNotificationService, + ) { + } + + public async createNotification( + notifieeId: User['id'], + type: Notification['type'], + data: Partial, + ): Promise { + if (data.notifierId && (notifieeId === data.notifierId)) { + return null; + } + + const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); + + const isMuted = profile?.mutingNotificationTypes.includes(type); + + // Create notification + const notification = await this.notificationsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + notifieeId: notifieeId, + type: type, + // 相手がこの通知をミュートしているようなら、既読を予めつけておく + isRead: isMuted, + ...data, + } as Partial) + .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); + + const packed = await this.notificationEntityService.pack(notification, {}); + + // Publish notification event + this.globalEventServie.publishMainStream(notifieeId, 'notification', packed); + + // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する + setTimeout(async () => { + const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); + if (fresh == null) return; // 既に削除されているかもしれない + if (fresh.isRead) return; + + //#region ただしミュートしているユーザーからの通知なら無視 + const mutings = await this.mutingsRepository.findBy({ + muterId: notifieeId, + }); + if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { + return; + } + //#endregion + + this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed); + this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); + + if (type === 'follow') this.#emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + if (type === 'receiveFollowRequest') this.#emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + }, 2000); + + return notification; + } + + // TODO + //const locales = await import('../../../../locales/index.js'); + + // TODO: locale ファイルをクライアント用とサーバー用で分けたい + + async #emailNotificationFollow(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } + + async #emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } +} 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; + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts new file mode 100644 index 0000000000..83e28baea4 --- /dev/null +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -0,0 +1,175 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In, IsNull } from 'typeorm'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { IdService } from '@/core/IdService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { Cache } from '@/misc/cache.js'; +import { query } from '@/misc/prelude/url.js'; +import type { Note } from '@/models/entities/Note.js'; +import { EmojisRepository } from '@/models/index.js'; +import { UtilityService } from './UtilityService.js'; +import { ReactionService } from './ReactionService.js'; + +/** + * 添付用絵文字情報 + */ +type PopulatedEmoji = { + name: string; + url: string; +}; + +@Injectable() +export class CustomEmojiService { + #cache: Cache; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private idService: IdService, + private globalEventServie: GlobalEventService, + private utilityService: UtilityService, + private reactionService: ReactionService, + ) { + this.#cache = new Cache(1000 * 60 * 60 * 12); + } + + public async add(data: { + driveFile: DriveFile; + name: string; + category: string | null; + aliases: string[]; + host: string | null; + }): Promise { + const emoji = await this.emojisRepository.insert({ + id: this.idService.genId(), + updatedAt: new Date(), + name: data.name, + category: data.category, + host: data.host, + aliases: data.aliases, + originalUrl: data.driveFile.url, + publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, + type: data.driveFile.webpublicType ?? data.driveFile.type, + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + + await this.db.queryResultCache!.remove(['meta_emojis']); + + return emoji; + } + + #normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { + // クエリに使うホスト + let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) + : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) + : this.utilityService.isSelfHost(src) ? null // 自ホスト指定 + : (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) + + host = this.utilityService.toPunyNullable(host); + + return host; + } + + #parseEmojiStr(emojiName: string, noteUserHost: string | null) { + const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); + if (!match) return { name: null, host: null }; + + const name = match[1]; + + // ホスト正規化 + const host = this.utilityService.toPunyNullable(this.#normalizeHost(match[2], noteUserHost)); + + return { name, host }; + } + + /** + * 添付用絵文字情報を解決する + * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) + * @param noteUserHost ノートやユーザープロフィールの所有者のホスト + * @returns 絵文字情報, nullは未マッチを意味する + */ + public async populateEmoji(emojiName: string, noteUserHost: string | null): Promise { + const { name, host } = this.#parseEmojiStr(emojiName, noteUserHost); + if (name == null) return null; + + const queryOrNull = async () => (await this.emojisRepository.findOneBy({ + name, + host: host ?? IsNull(), + })) ?? null; + + const emoji = await this.#cache.fetch(`${name} ${host}`, queryOrNull); + + if (emoji == null) return null; + + const isLocal = emoji.host == null; + const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため + const url = isLocal ? emojiUrl : `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; + + return { + name: emojiName, + url, + }; + } + + /** + * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される) + */ + 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); + } + + public aggregateNoteEmojis(notes: Note[]) { + let emojis: { name: string | null; host: string | null; }[] = []; + for (const note of notes) { + emojis = emojis.concat(note.emojis + .map(e => this.#parseEmojiStr(e, note.userHost))); + if (note.renote) { + emojis = emojis.concat(note.renote.emojis + .map(e => this.#parseEmojiStr(e, note.renote!.userHost))); + if (note.renote.user) { + emojis = emojis.concat(note.renote.user.emojis + .map(e => this.#parseEmojiStr(e, note.renote!.userHost))); + } + } + const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis; + emojis = emojis.concat(customReactions); + if (note.user) { + emojis = emojis.concat(note.user.emojis + .map(e => this.#parseEmojiStr(e, note.userHost))); + } + } + return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[]; + } + + /** + * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します + */ + 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[] = []; + const hosts = new Set(notCachedEmojis.map(e => e.host)); + for (const host of hosts) { + emojisQuery.push({ + name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)), + host: host ?? IsNull(), + }); + } + const _emojis = emojisQuery.length > 0 ? await this.emojisRepository.find({ + where: emojisQuery, + select: ['name', 'host', 'originalUrl', 'publicUrl'], + }) : []; + for (const emoji of _emojis) { + this.#cache.set(`${emoji.name} ${emoji.host}`, emoji); + } + } +} diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts new file mode 100644 index 0000000000..ba67bc499e --- /dev/null +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { QueueService } from '@/core/QueueService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; + +@Injectable() +export class DeleteAccountService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userSuspendService: UserSuspendService, + private queueService: QueueService, + private globalEventServie: GlobalEventService, + ) { + } + + public async deleteAccount(user: { + id: string; + host: string | null; + }): Promise { + // 物理削除する前にDelete activityを送信する + await this.userSuspendService.doPostSuspend(user).catch(e => {}); + + this.queueService.createDeleteAccountJob(user, { + soft: false, + }); + + await this.usersRepository.update(user.id, { + isDeleted: true, + }); + + // Terminate streaming + this.globalEventServie.publishUserEvent(user.id, 'terminate', {}); + } +} diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts new file mode 100644 index 0000000000..84d5ca2e8b --- /dev/null +++ b/packages/backend/src/core/DownloadService.ts @@ -0,0 +1,123 @@ +import * as fs from 'node:fs'; +import * as stream from 'node:stream'; +import * as util from 'node:util'; +import { Inject, Injectable } from '@nestjs/common'; +import IPCIDR from 'ip-cidr'; +import PrivateIp from 'private-ip'; +import got, * as Got from 'got'; +import chalk from 'chalk'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import Logger from '@/logger.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { StatusError } from '@/misc/status-error.js'; + +const pipeline = util.promisify(stream.pipeline); + +@Injectable() +export class DownloadService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + this.#logger = new Logger('download'); + } + + public async downloadUrl(url: string, path: string): Promise { + this.#logger.info(`Downloading ${chalk.cyan(url)} ...`); + + const timeout = 30 * 1000; + const operationTimeout = 60 * 1000; + const maxSize = this.config.maxFileSize ?? 262144000; + + const req = got.stream(url, { + headers: { + 'User-Agent': this.config.userAgent, + }, + timeout: { + lookup: timeout, + connect: timeout, + secureConnect: timeout, + socket: timeout, // read timeout + response: timeout, + send: timeout, + request: operationTimeout, // whole operation timeout + }, + agent: { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + }, + http2: false, // default + retry: { + limit: 0, + }, + }).on('response', (res: Got.Response) => { + if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { + if (this.#isPrivateIp(res.ip)) { + this.#logger.warn(`Blocked address: ${res.ip}`); + req.destroy(); + } + } + + const contentLength = res.headers['content-length']; + if (contentLength != null) { + const size = Number(contentLength); + if (size > maxSize) { + this.#logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); + req.destroy(); + } + } + }).on('downloadProgress', (progress: Got.Progress) => { + if (progress.transferred > maxSize) { + this.#logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); + req.destroy(); + } + }); + + try { + await pipeline(req, fs.createWriteStream(path)); + } catch (e) { + if (e instanceof Got.HTTPError) { + throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); + } else { + throw e; + } + } + + this.#logger.succ(`Download finished: ${chalk.cyan(url)}`); + } + + public async downloadTextFile(url: string): Promise { + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`text file: Temp file is ${path}`); + + try { + // write content at URL to temp file + await this.downloadUrl(url, path); + + const text = await util.promisify(fs.readFile)(path, 'utf8'); + + return text; + } finally { + cleanup(); + } + } + + #isPrivateIp(ip: string): boolean { + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = new IPCIDR(net); + if (cidr.contains(ip)) { + return false; + } + } + + return PrivateIp(ip); + } +} diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts new file mode 100644 index 0000000000..f54412ff41 --- /dev/null +++ b/packages/backend/src/core/DriveService.ts @@ -0,0 +1,740 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; +import sharp from 'sharp'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import Logger from '@/Logger.js'; +import type { IRemoteUser, User } from '@/models/entities/User.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; +import { IdService } from '@/core/IdService.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { contentDisposition } from '@/misc/content-disposition.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { VideoProcessingService } from '@/core/VideoProcessingService.js'; +import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import type { IImage } from '@/core/ImageProcessingService.js'; +import { QueueService } from '@/core/QueueService.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { createTemp } from '@/misc/create-temp.js'; +import DriveChart from '@/core/chart/charts/drive.js'; +import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { S3Service } from '@/core/S3Service.js'; +import { InternalStorageService } from '@/core/InternalStorageService.js'; +import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { FileInfoService } from './FileInfoService.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; + /** File path */ + path: string; + /** Name */ + name?: string | null; + /** Comment */ + comment?: string | null; + /** Folder ID */ + folderId?: any; + /** If set to true, forcibly upload the file even if there is a file with the same hash. */ + force?: boolean; + /** Do not save file to local */ + isLink?: boolean; + /** URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL) */ + url?: string | null; + /** URL of source (リモートインスタンスのURLからアップロードされた場合の元URL) */ + uri?: string | null; + /** Mark file as sensitive */ + sensitive?: boolean | null; + + requestIp?: string | null; + requestHeaders?: Record | null; +}; + +type UploadFromUrlArgs = { + url: string; + user: { id: User['id']; host: User['host'] } | null; + folderId?: DriveFolder['id'] | null; + uri?: string | null; + sensitive?: boolean; + force?: boolean; + isLink?: boolean; + comment?: string | null; + requestIp?: string | null; + requestHeaders?: Record | null; +}; + +@Injectable() +export class DriveService { + #registerLogger: Logger; + #downloaderLogger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private fileInfoService: FileInfoService, + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + private idService: IdService, + private metaService: MetaService, + private downloadService: DownloadService, + private internalStorageService: InternalStorageService, + private s3Service: S3Service, + private imageProcessingService: ImageProcessingService, + private videoProcessingService: VideoProcessingService, + private globalEventService: GlobalEventService, + private queueService: QueueService, + private driveChart: DriveChart, + private perUserDriveChart: PerUserDriveChart, + private instanceChart: InstanceChart, + ) { + const logger = new Logger('drive', 'blue'); + this.#registerLogger = logger.createSubLogger('register', 'yellow'); + this.#downloaderLogger = logger.createSubLogger('downloader'); + } + + /*** + * Save file + * @param path Path for original + * @param name Name for original + * @param type Content-Type for original + * @param hash Hash for original + * @param size Size for original + */ + 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); + + const meta = await this.metaService.fetch(); + + if (meta.useObjectStorage) { + //#region ObjectStorage params + let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); + + if (ext === '') { + if (type === 'image/jpeg') ext = '.jpg'; + if (type === 'image/png') ext = '.png'; + if (type === 'image/webp') ext = '.webp'; + if (type === 'image/apng') ext = '.apng'; + if (type === 'image/vnd.mozilla.apng') ext = '.apng'; + } + + // 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、 + // 許可されているファイル形式でしか拡張子をつけない + if (!FILE_TYPE_BROWSERSAFE.includes(type)) { + ext = ''; + } + + const baseUrl = meta.objectStorageBaseUrl + || `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; + + // for original + const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; + const url = `${ baseUrl }/${ key }`; + + // for alts + let webpublicKey: string | null = null; + let webpublicUrl: string | null = null; + let thumbnailKey: string | null = null; + let thumbnailUrl: string | null = null; + //#endregion + + //#region Uploads + this.#registerLogger.info(`uploading original: ${key}`); + const uploads = [ + this.#upload(key, fs.createReadStream(path), type, name), + ]; + + if (alts.webpublic) { + webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; + webpublicUrl = `${ baseUrl }/${ webpublicKey }`; + + this.#registerLogger.info(`uploading webpublic: ${webpublicKey}`); + uploads.push(this.#upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name)); + } + + if (alts.thumbnail) { + thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; + thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; + + this.#registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); + uploads.push(this.#upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type)); + } + + await Promise.all(uploads); + //#endregion + + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = key; + file.thumbnailAccessKey = thumbnailKey; + file.webpublicAccessKey = webpublicKey; + file.webpublicType = alts.webpublic?.type ?? null; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + file.storedInternal = false; + + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + } else { // use internal storage + const accessKey = uuid(); + const thumbnailAccessKey = 'thumbnail-' + uuid(); + const webpublicAccessKey = 'webpublic-' + uuid(); + + const url = this.internalStorageService.saveFromPath(accessKey, path); + + let thumbnailUrl: string | null = null; + let webpublicUrl: string | null = null; + + if (alts.thumbnail) { + thumbnailUrl = this.internalStorageService.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); + this.#registerLogger.info(`thumbnail stored: ${thumbnailAccessKey}`); + } + + if (alts.webpublic) { + webpublicUrl = this.internalStorageService.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); + this.#registerLogger.info(`web stored: ${webpublicAccessKey}`); + } + + file.storedInternal = true; + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = accessKey; + file.thumbnailAccessKey = thumbnailAccessKey; + file.webpublicAccessKey = webpublicAccessKey; + file.webpublicType = alts.webpublic?.type ?? null; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + } + } + + /** + * Generate webpublic, thumbnail, etc + * @param path Path for original + * @param type Content-Type for original + * @param generateWeb Generate webpublic or not + */ + public async generateAlts(path: string, type: string, generateWeb: boolean) { + if (type.startsWith('video/')) { + try { + const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path); + return { + webpublic: null, + thumbnail, + }; + } catch (err) { + this.#registerLogger.warn(`GenerateVideoThumbnail failed: ${err}`); + return { + webpublic: null, + thumbnail: null, + }; + } + } + + if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) { + this.#registerLogger.debug('web image and thumbnail not created (not an required file)'); + return { + webpublic: null, + thumbnail: null, + }; + } + + let img: sharp.Sharp | null = null; + let satisfyWebpublic: boolean; + + try { + img = sharp(path); + const metadata = await img.metadata(); + const isAnimated = metadata.pages && metadata.pages > 1; + + // skip animated + if (isAnimated) { + return { + webpublic: null, + thumbnail: null, + }; + } + + satisfyWebpublic = !!( + type !== 'image/svg+xml' && type !== 'image/webp' && + !(metadata.exif || metadata.iptc || metadata.xmp || metadata.tifftagPhotoshop) && + metadata.width && metadata.width <= 2048 && + metadata.height && metadata.height <= 2048 + ); + } catch (err) { + this.#registerLogger.warn(`sharp failed: ${err}`); + return { + webpublic: null, + thumbnail: null, + }; + } + + // #region webpublic + let webpublic: IImage | null = null; + + if (generateWeb && !satisfyWebpublic) { + this.#registerLogger.info('creating web image'); + + try { + if (['image/jpeg', 'image/webp'].includes(type)) { + webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048); + } else if (['image/png'].includes(type)) { + webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); + } else if (['image/svg+xml'].includes(type)) { + webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); + } else { + this.#registerLogger.debug('web image not created (not an required image)'); + } + } catch (err) { + this.#registerLogger.warn('web image not created (an error occured)', err as Error); + } + } else { + if (satisfyWebpublic) this.#registerLogger.info('web image not created (original satisfies webpublic)'); + else this.#registerLogger.info('web image not created (from remote)'); + } + // #endregion webpublic + + // #region thumbnail + let thumbnail: IImage | null = null; + + try { + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { + thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280); + } else { + this.#registerLogger.debug('thumbnail not created (not an required file)'); + } + } catch (err) { + this.#registerLogger.warn('thumbnail not created (an error occured)', err as Error); + } + // #endregion thumbnail + + return { + webpublic, + thumbnail, + }; + } + + /** + * Upload to ObjectStorage + */ + 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'; + + const meta = await this.metaService.fetch(); + + const params = { + Bucket: meta.objectStorageBucket, + Key: key, + Body: stream, + ContentType: type, + CacheControl: 'max-age=31536000, immutable', + } as S3.PutObjectRequest; + + if (filename) params.ContentDisposition = contentDisposition('inline', filename); + if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; + + const s3 = this.s3Service.getS3(meta); + + const upload = s3.upload(params, { + partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, + }); + + const result = await upload.promise(); + if (result) this.#registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); + } + + async #deleteOldFile(user: IRemoteUser) { + const q = this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }) + .andWhere('file.isLink = FALSE'); + + if (user.avatarId) { + q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); + } + + if (user.bannerId) { + q.andWhere('file.id != :bannerId', { bannerId: user.bannerId }); + } + + q.orderBy('file.id', 'ASC'); + + const oldFile = await q.getOne(); + + if (oldFile) { + this.deleteFile(oldFile, true); + } + } + + /** + * Add file to drive + * + */ + public async addFile({ + user, + path, + name = null, + comment = null, + folderId = null, + force = false, + isLink = false, + url = null, + uri = null, + sensitive = null, + requestIp = null, + requestHeaders = null, + }: AddFileArgs): Promise { + let skipNsfwCheck = false; + const instance = await this.metaService.fetch(); + if (user == null) skipNsfwCheck = true; + if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; + + const info = await this.fileInfoService.getFileInfo(path, { + skipSensitiveDetection: skipNsfwCheck, + sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる + instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + 0.5, + sensitiveThresholdForPorn: 0.75, + enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + }); + this.#registerLogger.info(`${JSON.stringify(info)}`); + + // 現状 false positive が多すぎて実用に耐えない + //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { + // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); + //} + + // detect name + const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); + + if (user && !force) { + // Check if there is a file with the same hash + const much = await this.driveFilesRepository.findOneBy({ + md5: info.md5, + userId: user.id, + }); + + if (much) { + this.#registerLogger.info(`file with same hash is found: ${much.id}`); + return much; + } + } + + //#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; + this.#registerLogger.debug('drive capacity override applied'); + this.#registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + } + + this.#registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); + + // If usage limit exceeded + if (usage + info.size > driveCapacity) { + if (this.userEntityService.isLocalUser(user)) { + throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); + } else { + // (アバターまたはバナーを含まず)最も古いファイルを削除する + this.#deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as IRemoteUser); + } + } + } + //#endregion + + const fetchFolder = async () => { + if (!folderId) { + return null; + } + + const driveFolder = await this.driveFoldersRepository.findOneBy({ + id: folderId, + userId: user ? user.id : IsNull(), + }); + + if (driveFolder == null) throw new Error('folder-not-found'); + + return driveFolder; + }; + + const properties: { + width?: number; + height?: number; + orientation?: number; + } = {}; + + if (info.width) { + properties['width'] = info.width; + properties['height'] = info.height; + } + if (info.orientation != null) { + properties['orientation'] = info.orientation; + } + + const profile = user ? await this.userProfilesRepository.findOneBy({ userId: user.id }) : null; + + const folder = await fetchFolder(); + + let file = new DriveFile(); + file.id = this.idService.genId(); + file.createdAt = new Date(); + file.userId = user ? user.id : null; + file.userHost = user ? user.host : null; + file.folderId = folder !== null ? folder.id : null; + file.comment = comment; + file.properties = properties; + file.blurhash = info.blurhash ?? null; + file.isLink = isLink; + file.requestIp = requestIp; + file.requestHeaders = requestHeaders; + file.maybeSensitive = info.sensitive; + file.maybePorn = info.porn; + file.isSensitive = user + ? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false + : false; + + if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; + if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; + + if (url !== null) { + file.src = url; + + if (isLink) { + file.url = url; + // ローカルプロキシ用 + file.accessKey = uuid(); + file.thumbnailAccessKey = 'thumbnail-' + uuid(); + file.webpublicAccessKey = 'webpublic-' + uuid(); + } + } + + if (uri !== null) { + file.uri = uri; + } + + if (isLink) { + try { + file.size = 0; + file.md5 = info.md5; + file.name = detectedName; + file.type = info.type.mime; + file.storedInternal = false; + + file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + } catch (err) { + // duplicate key error (when already registered) + if (isDuplicateKeyValueError(err)) { + this.#registerLogger.info(`already registered ${file.uri}`); + + file = await this.driveFilesRepository.findOneBy({ + uri: file.uri!, + userId: user ? user.id : IsNull(), + }) as DriveFile; + } else { + this.#registerLogger.error(err as Error); + throw err; + } + } + } else { + file = await (this.#save(file, path, detectedName, info.type.mime, info.md5, info.size)); + } + + this.#registerLogger.succ(`drive file has been created ${file.id}`); + + if (user) { + this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { + // Publish driveFileCreated event + this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile); + this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile); + }); + } + + // 統計を更新 + this.driveChart.update(file, true); + this.perUserDriveChart.update(file, true); + if (file.userHost !== null) { + this.instanceChart.updateDrive(file, true); + } + + return file; + } + + public async deleteFile(file: DriveFile, isExpired = false) { + if (file.storedInternal) { + this.internalStorageService.del(file.accessKey!); + + if (file.thumbnailUrl) { + this.internalStorageService.del(file.thumbnailAccessKey!); + } + + if (file.webpublicUrl) { + this.internalStorageService.del(file.webpublicAccessKey!); + } + } else if (!file.isLink) { + this.queueService.createDeleteObjectStorageFileJob(file.accessKey!); + + if (file.thumbnailUrl) { + this.queueService.createDeleteObjectStorageFileJob(file.thumbnailAccessKey!); + } + + if (file.webpublicUrl) { + this.queueService.createDeleteObjectStorageFileJob(file.webpublicAccessKey!); + } + } + + this.#deletePostProcess(file, isExpired); + } + + public async deleteFileSync(file: DriveFile, isExpired = false) { + if (file.storedInternal) { + this.internalStorageService.del(file.accessKey!); + + if (file.thumbnailUrl) { + this.internalStorageService.del(file.thumbnailAccessKey!); + } + + if (file.webpublicUrl) { + this.internalStorageService.del(file.webpublicAccessKey!); + } + } else if (!file.isLink) { + const promises = []; + + promises.push(this.deleteObjectStorageFile(file.accessKey!)); + + if (file.thumbnailUrl) { + promises.push(this.deleteObjectStorageFile(file.thumbnailAccessKey!)); + } + + if (file.webpublicUrl) { + promises.push(this.deleteObjectStorageFile(file.webpublicAccessKey!)); + } + + await Promise.all(promises); + } + + this.#deletePostProcess(file, isExpired); + } + + async #deletePostProcess(file: DriveFile, isExpired = false) { + // リモートファイル期限切れ削除後は直リンクにする + if (isExpired && file.userHost !== null && file.uri != null) { + this.driveFilesRepository.update(file.id, { + isLink: true, + url: file.uri, + thumbnailUrl: null, + webpublicUrl: null, + storedInternal: false, + // ローカルプロキシ用 + accessKey: uuid(), + thumbnailAccessKey: 'thumbnail-' + uuid(), + webpublicAccessKey: 'webpublic-' + uuid(), + }); + } else { + this.driveFilesRepository.delete(file.id); + } + + // 統計を更新 + this.driveChart.update(file, false); + this.perUserDriveChart.update(file, false); + if (file.userHost !== null) { + this.instanceChart.updateDrive(file, false); + } + } + + public async deleteObjectStorageFile(key: string) { + const meta = await this.metaService.fetch(); + + const s3 = this.s3Service.getS3(meta); + + await s3.deleteObject({ + Bucket: meta.objectStorageBucket!, + Key: key, + }).promise(); + } + + public async uploadFromUrl({ + url, + user, + folderId = null, + uri = null, + sensitive = false, + force = false, + isLink = false, + comment = null, + requestIp = null, + requestHeaders = null, + }: UploadFromUrlArgs): Promise { + let name = new URL(url).pathname.split('/').pop() ?? null; + if (name == null || !this.driveFileEntityService.validateFileName(name)) { + name = null; + } + + // If the comment is same as the name, skip comment + // (image.name is passed in when receiving attachment) + if (comment !== null && name === comment) { + comment = null; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + try { + // write content at URL to temp file + await this.downloadService.downloadUrl(url, path); + + const driveFile = await this.addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); + this.#downloaderLogger.succ(`Got: ${driveFile.id}`); + return driveFile!; + } catch (err) { + this.#downloaderLogger.error(`Failed to create drive file: ${err}`, { + url: url, + e: err, + }); + throw err; + } finally { + cleanup(); + } + } +} diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts new file mode 100644 index 0000000000..7d6960b73b --- /dev/null +++ b/packages/backend/src/core/EmailService.ts @@ -0,0 +1,175 @@ +import * as nodemailer from 'nodemailer'; +import { Inject, Injectable } from '@nestjs/common'; +import { validate as validateEmail } from 'deep-email-validator'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import Logger from '@/logger.js'; +import { UserProfilesRepository } from '@/models/index.js'; + +@Injectable() +export class EmailService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private metaService: MetaService, + ) { + this.#logger = new Logger('email'); + } + + public async sendEmail(to: string, subject: string, html: string, text: string) { + const meta = await this.metaService.fetch(true); + + const iconUrl = `${this.config.url}/static-assets/mi-white.png`; + const emailSettingUrl = `${this.config.url}/settings/email`; + + const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + + const transporter = nodemailer.createTransport({ + host: meta.smtpHost, + port: meta.smtpPort, + secure: meta.smtpSecure, + ignoreTLS: !enableAuth, + proxy: this.config.proxySmtp, + auth: enableAuth ? { + user: meta.smtpUser, + pass: meta.smtpPass, + } : undefined, + } as any); + + try { + // TODO: htmlサニタイズ + const info = await transporter.sendMail({ + from: meta.email!, + to: to, + subject: subject, + text: text, + html: ` + + + + ${ subject } + + + +
+
+ +
+
+

${ subject }

+
${ html }
+
+ +
+ + +`, + }); + + this.#logger.info(`Message sent: ${info.messageId}`); + } catch (err) { + this.#logger.error(err as Error); + throw err; + } + } + + public async validateEmailForAccount(emailAddress: string): Promise<{ + available: boolean; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; + }> { + const meta = await this.metaService.fetch(); + + const exist = await this.userProfilesRepository.countBy({ + emailVerified: true, + email: emailAddress, + }); + + const validated = meta.enableActiveEmailValidation ? await validateEmail({ + email: emailAddress, + validateRegex: true, + validateMx: true, + validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので + validateDisposable: true, // 捨てアドかどうかチェック + validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので + }) : { valid: true }; + + const available = exist === 0 && validated.valid; + + return { + available, + reason: available ? null : + exist !== 0 ? 'used' : + validated.reason === 'regex' ? 'format' : + validated.reason === 'disposable' ? 'disposable' : + validated.reason === 'mx' ? 'mx' : + validated.reason === 'smtp' ? 'smtp' : + null, + }; + } +} diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts new file mode 100644 index 0000000000..24bedd8192 --- /dev/null +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { InstancesRepository } from '@/models/index.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import { Cache } from '@/misc/cache.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { UtilityService } from './UtilityService.js'; + +@Injectable() +export class FederatedInstanceService { + #cache: Cache; + + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private utilityService: UtilityService, + private idService: IdService, + ) { + this.#cache = new Cache(1000 * 60 * 60); + } + + public async registerOrFetchInstanceDoc(host: string): Promise { + host = this.utilityService.toPuny(host); + + const cached = this.#cache.get(host); + if (cached) return cached; + + const index = await this.instancesRepository.findOneBy({ host }); + + if (index == null) { + const i = await this.instancesRepository.insert({ + id: this.idService.genId(), + host, + caughtAt: new Date(), + lastCommunicatedAt: new Date(), + }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); + + this.#cache.set(host, i); + return i; + } else { + this.#cache.set(host, index); + return index; + } + } +} diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts new file mode 100644 index 0000000000..6353784c13 --- /dev/null +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -0,0 +1,283 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import { JSDOM } from 'jsdom'; +import fetch from 'node-fetch'; +import tinycolor from 'tinycolor2'; +import type { Instance } from '@/models/entities/Instance.js'; +import { InstancesRepository } from '@/models/index.js'; +import { AppLockService } from '@/core/AppLockService.js'; +import Logger from '@/logger.js'; +import { DI } from '@/di-symbols.js'; +import { HttpRequestService } from './HttpRequestService.js'; +import type { DOMWindow } from 'jsdom'; + +const logger = new Logger('metadata', 'cyan'); + +type NodeInfo = { + openRegistrations?: any; + software?: { + name?: any; + version?: any; + }; + metadata?: { + name?: any; + nodeName?: any; + nodeDescription?: any; + description?: any; + maintainer?: { + name?: any; + email?: any; + }; + }; +}; + +@Injectable() +export class FetchInstanceMetadataService { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private appLockService: AppLockService, + private httpRequestService: HttpRequestService, + ) { + } + + public async fetchInstanceMetadata(instance: Instance, force = false): Promise { + const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host); + + if (!force) { + const _instance = await this.instancesRepository.findOneBy({ host: instance.host }); + const now = Date.now(); + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { + unlock(); + return; + } + } + + logger.info(`Fetching metadata of ${instance.host} ...`); + + try { + const [info, dom, manifest] = await Promise.all([ + this.#fetchNodeinfo(instance).catch(() => null), + this.#fetchDom(instance).catch(() => null), + this.#fetchManifest(instance).catch(() => null), + ]); + + const [favicon, icon, themeColor, name, description] = await Promise.all([ + this.#fetchFaviconUrl(instance, dom).catch(() => null), + this.#fetchIconUrl(instance, dom, manifest).catch(() => null), + this.#getThemeColor(info, dom, manifest).catch(() => null), + this.#getSiteName(info, dom, manifest).catch(() => null), + this.#getDescription(info, dom, manifest).catch(() => null), + ]); + + logger.succ(`Successfuly fetched metadata of ${instance.host}`); + + const updates = { + infoUpdatedAt: new Date(), + } as Record; + + if (info) { + updates.softwareName = info.software?.name.toLowerCase(); + updates.softwareVersion = info.software?.version; + updates.openRegistrations = info.openRegistrations; + updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; + updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; + } + + if (name) updates.name = name; + if (description) updates.description = description; + if (icon || favicon) updates.iconUrl = icon ?? favicon; + if (favicon) updates.faviconUrl = favicon; + if (themeColor) updates.themeColor = themeColor; + + await this.instancesRepository.update(instance.id, updates); + + logger.succ(`Successfuly updated metadata of ${instance.host}`); + } catch (e) { + logger.error(`Failed to update metadata of ${instance.host}: ${e}`); + } finally { + unlock(); + } + } + + async #fetchNodeinfo(instance: Instance): Promise { + logger.info(`Fetching nodeinfo of ${instance.host} ...`); + + try { + const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') + .catch(err => { + if (err.statusCode === 404) { + throw 'No nodeinfo provided'; + } else { + throw err.statusCode ?? err.message; + } + }) as Record; + + if (wellknown.links == null || !Array.isArray(wellknown.links)) { + throw 'No wellknown links'; + } + + const links = wellknown.links as any[]; + + const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); + const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); + const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); + const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; + + if (link == null) { + throw 'No nodeinfo link provided'; + } + + const info = await this.httpRequestService.getJson(link.href) + .catch(err => { + throw err.statusCode ?? err.message; + }); + + logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); + + return info as NodeInfo; + } catch (err) { + logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); + + throw err; + } + } + + async #fetchDom(instance: Instance): Promise { + logger.info(`Fetching HTML of ${instance.host} ...`); + + const url = 'https://' + instance.host; + + const html = await this.httpRequestService.getHtml(url); + + const { window } = new JSDOM(html); + const doc = window.document; + + return doc; + } + + async #fetchManifest(instance: Instance): Promise | null> { + const url = 'https://' + instance.host; + + const manifestUrl = url + '/manifest.json'; + + const manifest = await this.httpRequestService.getJson(manifestUrl) as Record; + + return manifest; + } + + async #fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise { + const url = 'https://' + instance.host; + + if (doc) { + // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 + const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; + + if (href) { + return (new URL(href, url)).href; + } + } + + const faviconUrl = url + '/favicon.ico'; + + const favicon = await fetch(faviconUrl, { + // TODO + //timeout: 10000, + agent: url => this.httpRequestService.getAgentByUrl(url), + }); + + if (favicon.ok) { + return faviconUrl; + } + + return null; + } + + 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; + return (new URL(manifest.icons[0].src, url)).href; + } + + if (doc) { + const url = 'https://' + instance.host; + + // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 + const links = Array.from(doc.getElementsByTagName('link')).reverse(); + // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 + const href = + [ + links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, + links.find(link => link.relList.contains('apple-touch-icon'))?.href, + links.find(link => link.relList.contains('icon'))?.href, + ] + .find(href => href); + + if (href) { + return (new URL(href, url)).href; + } + } + + return null; + } + + 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; + + if (themeColor) { + const color = new tinycolor(themeColor); + if (color.isValid()) return color.toHexString(); + } + + return null; + } + + async #getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (info && info.metadata) { + if (info.metadata.nodeName || info.metadata.name) { + return info.metadata.nodeName ?? info.metadata.name; + } + } + + if (doc) { + const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); + + if (og) { + return og; + } + } + + if (manifest) { + return manifest.name ?? manifest.short_name; + } + + return null; + } + + async #getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (info && info.metadata) { + if (info.metadata.nodeDescription || info.metadata.description) { + return info.metadata.nodeDescription ?? info.metadata.description; + } + } + + if (doc) { + const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); + if (meta) { + return meta; + } + + const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); + if (og) { + return og; + } + } + + if (manifest) { + return manifest.name ?? manifest.short_name; + } + + return null; + } +} diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts new file mode 100644 index 0000000000..45c94dafdc --- /dev/null +++ b/packages/backend/src/core/FileInfoService.ts @@ -0,0 +1,382 @@ +import * as fs from 'node:fs'; +import * as crypto from 'node:crypto'; +import { join } from 'node:path'; +import * as stream from 'node:stream'; +import * as util from 'node:util'; +import { Inject, Injectable } from '@nestjs/common'; +import { FSWatcher } from 'chokidar'; +import { fileTypeFromFile } from 'file-type'; +import FFmpeg from 'fluent-ffmpeg'; +import isSvg from 'is-svg'; +import probeImageSize from 'probe-image-size'; +import { type predictionType } from 'nsfwjs'; +import sharp from 'sharp'; +import { encode } from 'blurhash'; +import { createTempDir } from '@/misc/create-temp.js'; +import { AiService } from '@/core/AiService.js'; + +const pipeline = util.promisify(stream.pipeline); + +export type FileInfo = { + size: number; + md5: string; + type: { + mime: string; + ext: string | null; + }; + width?: number; + height?: number; + orientation?: number; + blurhash?: string; + sensitive: boolean; + porn: boolean; + warnings: string[]; +}; + +const TYPE_OCTET_STREAM = { + mime: 'application/octet-stream', + ext: null, +}; + +const TYPE_SVG = { + mime: 'image/svg+xml', + ext: 'svg', +}; +@Injectable() +export class FileInfoService { + constructor( + private aiService: AiService, + ) { + } + + /** + * Get file information + */ + public async getFileInfo(path: string, opts: { + skipSensitiveDetection: boolean; + sensitiveThreshold?: number; + sensitiveThresholdForPorn?: number; + enableSensitiveMediaDetectionForVideos?: boolean; + }): Promise { + const warnings = [] as string[]; + + const size = await this.getFileSize(path); + const md5 = await this.#calcHash(path); + + let type = await this.detectType(path); + + // image dimensions + let width: number | undefined; + let height: number | undefined; + let orientation: number | undefined; + + if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { + const imageSize = await this.#detectImageSize(path).catch(e => { + warnings.push(`detectImageSize failed: ${e}`); + return undefined; + }); + + // うまく判定できない画像は octet-stream にする + if (!imageSize) { + warnings.push('cannot detect image dimensions'); + type = TYPE_OCTET_STREAM; + } else if (imageSize.wUnits === 'px') { + width = imageSize.width; + height = imageSize.height; + orientation = imageSize.orientation; + + // 制限を超えている画像は octet-stream にする + if (imageSize.width > 16383 || imageSize.height > 16383) { + warnings.push('image dimensions exceeds limits'); + type = TYPE_OCTET_STREAM; + } + } else { + warnings.push(`unsupported unit type: ${imageSize.wUnits}`); + } + } + + let blurhash: string | undefined; + + if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) { + blurhash = await this.#getBlurhash(path).catch(e => { + warnings.push(`getBlurhash failed: ${e}`); + return undefined; + }); + } + + let sensitive = false; + let porn = false; + + if (!opts.skipSensitiveDetection) { + await this.#detectSensitivity( + path, + type.mime, + opts.sensitiveThreshold ?? 0.5, + opts.sensitiveThresholdForPorn ?? 0.75, + opts.enableSensitiveMediaDetectionForVideos ?? false, + ).then(value => { + [sensitive, porn] = value; + }, error => { + warnings.push(`detectSensitivity failed: ${error}`); + }); + } + + return { + size, + md5, + type, + width, + height, + orientation, + blurhash, + sensitive, + porn, + warnings, + }; + } + + async #detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { + let sensitive = false; + let porn = false; + + function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { + let sensitive = false; + let porn = false; + + if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; + + return [sensitive, porn]; + } + + if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) { + const result = await this.aiService.detectSensitive(source); + if (result) { + [sensitive, porn] = judgePrediction(result); + } + } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { + const [outDir, disposeOutDir] = await createTempDir(); + try { + const command = FFmpeg() + .input(source) + .inputOptions([ + '-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) + '-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) + ]) + .noAudio() + .videoFilters([ + { + filter: 'select', // フレームのフィルタリング + options: { + e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい) + }, + }, + { + filter: 'blackframe', // 暗いフレームの検出 + options: { + amount: '0', // 暗さに関わらず全てのフレームで測定値を取る + }, + }, + { + filter: 'metadata', + options: { + mode: 'select', // フレーム選択モード + key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) + value: '50', + function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので) + }, + }, + { + filter: 'scale', + options: { + w: 299, + h: 299, + }, + }, + ]) + .format('image2') + .output(join(outDir, '%d.png')) + .outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない + const results: ReturnType[] = []; + let frameIndex = 0; + let targetIndex = 0; + let nextIndex = 1; + for await (const path of this.#asyncIterateFrames(outDir, command)) { + try { + const index = frameIndex++; + if (index !== targetIndex) { + continue; + } + targetIndex = nextIndex; + nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける + const result = await this.aiService.detectSensitive(path); + if (result) { + results.push(judgePrediction(result)); + } + } finally { + fs.promises.unlink(path); + } + } + sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); + porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); + } finally { + disposeOutDir(); + } + } + + return [sensitive, porn]; + } + + async *#asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { + const watcher = new FSWatcher({ + cwd, + disableGlobbing: true, + }); + let finished = false; + command.once('end', () => { + finished = true; + watcher.close(); + }); + command.run(); + for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition + const current = `${i}.png`; + const next = `${i + 1}.png`; + const framePath = join(cwd, current); + if (await this.#exists(join(cwd, next))) { + yield framePath; + } else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition + watcher.add(next); + await new Promise((resolve, reject) => { + watcher.on('add', function onAdd(path) { + if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている + watcher.unwatch(current); + watcher.off('add', onAdd); + resolve(); + } + }); + command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている + command.once('error', reject); + }); + yield framePath; + } else if (await this.#exists(framePath)) { + yield framePath; + } else { + return; + } + } + } + + #exists(path: string): Promise { + return fs.promises.access(path).then(() => true, () => false); + } + + /** + * Detect MIME Type and extension + */ + public async detectType(path: string): Promise<{ + mime: string; + ext: string | null; +}> { + // Check 0 byte + const fileSize = await this.getFileSize(path); + if (fileSize === 0) { + return TYPE_OCTET_STREAM; + } + + const type = await fileTypeFromFile(path); + + if (type) { + // XMLはSVGかもしれない + if (type.mime === 'application/xml' && await this.checkSvg(path)) { + return TYPE_SVG; + } + + return { + mime: type.mime, + ext: type.ext, + }; + } + + // 種類が不明でもSVGかもしれない + if (await this.checkSvg(path)) { + return TYPE_SVG; + } + + // それでも種類が不明なら application/octet-stream にする + return TYPE_OCTET_STREAM; + } + + /** + * Check the file is SVG or not + */ + public async checkSvg(path: string) { + try { + const size = await this.getFileSize(path); + if (size > 1 * 1024 * 1024) return false; + return isSvg(fs.readFileSync(path)); + } catch { + return false; + } + } + + /** + * Get file size + */ + public async getFileSize(path: string): Promise { + const getStat = util.promisify(fs.stat); + return (await getStat(path)).size; + } + + /** + * Calculate MD5 hash + */ + async #calcHash(path: string): Promise { + const hash = crypto.createHash('md5').setEncoding('hex'); + await pipeline(fs.createReadStream(path), hash); + return hash.read(); + } + + /** + * Detect dimensions of image + */ + async #detectImageSize(path: string): Promise<{ + width: number; + height: number; + wUnits: string; + hUnits: string; + orientation?: number; +}> { + const readable = fs.createReadStream(path); + const imageSize = await probeImageSize(readable); + readable.destroy(); + return imageSize; + } + + /** + * Calculate average color of image + */ + #getBlurhash(path: string): Promise { + return new Promise((resolve, reject) => { + sharp(path) + .raw() + .ensureAlpha() + .resize(64, 64, { fit: 'inside' }) + .toBuffer((err, buffer, { width, height }) => { + if (err) return reject(err); + + let hash; + + try { + hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7); + } catch (e) { + return reject(e); + } + + resolve(hash); + }); + }); + } +} diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts new file mode 100644 index 0000000000..c36de63fde --- /dev/null +++ b/packages/backend/src/core/GlobalEventService.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import type { + StreamChannels, + AdminStreamTypes, + AntennaStreamTypes, + BroadcastTypes, + ChannelStreamTypes, + DriveStreamTypes, + GroupMessagingStreamTypes, + InternalStreamTypes, + MainStreamTypes, + MessagingIndexStreamTypes, + MessagingStreamTypes, + NoteStreamTypes, + UserListStreamTypes, + UserStreamTypes, +} from '@/server/api/stream/types.js'; +import type { Packed } from '@/misc/schema.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; + +@Injectable() +export class GlobalEventService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + ) { + } + + private publish(channel: StreamChannels, type: string | null, value?: any): void { + const message = type == null ? value : value == null ? + { type: type, body: null } : + { type: type, body: value }; + + this.redisClient.publish(this.config.host, JSON.stringify({ + channel: channel, + message: message, + })); + } + + public publishInternalEvent(type: K, value?: InternalStreamTypes[K]): void { + this.publish('internal', type, typeof value === 'undefined' ? null : value); + } + + public publishUserEvent(userId: User['id'], type: K, value?: UserStreamTypes[K]): void { + this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishBroadcastStream(type: K, value?: BroadcastTypes[K]): void { + this.publish('broadcast', type, typeof value === 'undefined' ? null : value); + } + + public publishMainStream(userId: User['id'], type: K, value?: MainStreamTypes[K]): void { + this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishDriveStream(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void { + this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishNoteStream(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void { + this.publish(`noteStream:${noteId}`, type, { + id: noteId, + body: value, + }); + } + + public publishChannelStream(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void { + this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishUserListStream(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void { + this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishAntennaStream(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void { + this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingStream(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void { + this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishGroupMessagingStream(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void { + this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingIndexStream(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void { + this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishNotesStream(note: Packed<'Note'>): void { + this.publish('notesStream', null, note); + } + + public publishAdminStream(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void { + this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } +} diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts new file mode 100644 index 0000000000..f6c06d48f4 --- /dev/null +++ b/packages/backend/src/core/HashtagService.ts @@ -0,0 +1,147 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/entities/User.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { IdService } from '@/core/IdService.js'; +import type { Hashtag } from '@/models/entities/Hashtag.js'; +import HashtagChart from '@/core/chart/charts/hashtag.js'; +import { HashtagsRepository, UsersRepository } from '@/models/index.js'; +import { UserEntityService } from './entities/UserEntityService.js'; + +@Injectable() +export class HashtagService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private hashtagChart: HashtagChart, + ) { + } + + public async updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) { + for (const tag of tags) { + await this.updateHashtag(user, tag); + } + } + + public async updateUsertags(user: User, tags: string[]) { + for (const tag of tags) { + await this.updateHashtag(user, tag, true, true); + } + + for (const tag of (user.tags ?? []).filter(x => !tags.includes(x))) { + await this.updateHashtag(user, tag, true, false); + } + } + + public async updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) { + tag = normalizeForSearch(tag); + + const index = await this.hashtagsRepository.findOneBy({ name: tag }); + + if (index == null && !inc) return; + + if (index != null) { + const q = this.hashtagsRepository.createQueryBuilder('tag').update() + .where('name = :name', { name: tag }); + + const set = {} as any; + + if (isUserAttached) { + if (inc) { + // 自分が初めてこのタグを使ったなら + if (!index.attachedUserIds.some(id => id === user.id)) { + set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`; + set.attachedUsersCount = () => '"attachedUsersCount" + 1'; + } + // 自分が(ローカル内で)初めてこのタグを使ったなら + if (this.userEntityService.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) { + set.attachedLocalUserIds = () => `array_append("attachedLocalUserIds", '${user.id}')`; + set.attachedLocalUsersCount = () => '"attachedLocalUsersCount" + 1'; + } + // 自分が(リモートで)初めてこのタグを使ったなら + if (this.userEntityService.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) { + set.attachedRemoteUserIds = () => `array_append("attachedRemoteUserIds", '${user.id}')`; + set.attachedRemoteUsersCount = () => '"attachedRemoteUsersCount" + 1'; + } + } else { + set.attachedUserIds = () => `array_remove("attachedUserIds", '${user.id}')`; + set.attachedUsersCount = () => '"attachedUsersCount" - 1'; + if (this.userEntityService.isLocalUser(user)) { + set.attachedLocalUserIds = () => `array_remove("attachedLocalUserIds", '${user.id}')`; + set.attachedLocalUsersCount = () => '"attachedLocalUsersCount" - 1'; + } else { + set.attachedRemoteUserIds = () => `array_remove("attachedRemoteUserIds", '${user.id}')`; + set.attachedRemoteUsersCount = () => '"attachedRemoteUsersCount" - 1'; + } + } + } else { + // 自分が初めてこのタグを使ったなら + if (!index.mentionedUserIds.some(id => id === user.id)) { + set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`; + set.mentionedUsersCount = () => '"mentionedUsersCount" + 1'; + } + // 自分が(ローカル内で)初めてこのタグを使ったなら + if (this.userEntityService.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) { + set.mentionedLocalUserIds = () => `array_append("mentionedLocalUserIds", '${user.id}')`; + set.mentionedLocalUsersCount = () => '"mentionedLocalUsersCount" + 1'; + } + // 自分が(リモートで)初めてこのタグを使ったなら + if (this.userEntityService.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) { + set.mentionedRemoteUserIds = () => `array_append("mentionedRemoteUserIds", '${user.id}')`; + set.mentionedRemoteUsersCount = () => '"mentionedRemoteUsersCount" + 1'; + } + } + + if (Object.keys(set).length > 0) { + q.set(set); + q.execute(); + } + } else { + if (isUserAttached) { + this.hashtagsRepository.insert({ + id: this.idService.genId(), + name: tag, + mentionedUserIds: [], + mentionedUsersCount: 0, + mentionedLocalUserIds: [], + mentionedLocalUsersCount: 0, + mentionedRemoteUserIds: [], + mentionedRemoteUsersCount: 0, + attachedUserIds: [user.id], + attachedUsersCount: 1, + attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], + attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, + attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], + attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, + } as Hashtag); + } else { + this.hashtagsRepository.insert({ + id: this.idService.genId(), + name: tag, + mentionedUserIds: [user.id], + mentionedUsersCount: 1, + mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], + mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, + mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], + mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, + attachedUserIds: [], + attachedUsersCount: 0, + attachedLocalUserIds: [], + attachedLocalUsersCount: 0, + attachedRemoteUserIds: [], + attachedRemoteUsersCount: 0, + } as Hashtag); + } + } + + if (!isUserAttached) { + this.hashtagChart.update(tag, user); + } + } +} diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts new file mode 100644 index 0000000000..21cde12536 --- /dev/null +++ b/packages/backend/src/core/HttpRequestService.ts @@ -0,0 +1,154 @@ +import * as http from 'node:http'; +import * as https from 'node:https'; +import CacheableLookup from 'cacheable-lookup'; +import fetch from 'node-fetch'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { StatusError } from '@/misc/status-error.js'; +import type { Response } from 'node-fetch'; +import type { URL } from 'node:url'; + +@Injectable() +export class HttpRequestService { + /** + * Get http non-proxy agent + */ + #http: http.Agent; + + /** + * Get https non-proxy agent + */ + #https: https.Agent; + + /** + * Get http proxy or non-proxy agent + */ + public httpAgent: http.Agent; + + /** + * Get https proxy or non-proxy agent + */ + public httpsAgent: https.Agent; + + constructor( + @Inject(DI.config) + private config: Config, + ) { + const cache = new CacheableLookup({ + maxTtl: 3600, // 1hours + errorTtl: 30, // 30secs + lookup: false, // nativeのdns.lookupにfallbackしない + }); + + this.#http = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + lookup: cache.lookup, + } as http.AgentOptions); + + this.#https = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + lookup: cache.lookup, + } as https.AgentOptions); + + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); + + this.httpAgent = config.proxy + ? new HttpProxyAgent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + maxSockets, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: config.proxy, + }) + : this.#http; + + this.httpsAgent = config.proxy + ? new HttpsProxyAgent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + maxSockets, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: config.proxy, + }) + : this.#https; + } + + /** + * Get agent by URL + * @param url URL + * @param bypassProxy Allways bypass proxy + */ + 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; + } else { + return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent; + } + } + + public async getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record): Promise { + const res = await this.getResponse({ + url, + method: 'GET', + headers: Object.assign({ + 'User-Agent': this.config.userAgent, + Accept: accept, + }, headers ?? {}), + timeout, + }); + + return await res.json(); + } + + public async getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record): Promise { + const res = await this.getResponse({ + url, + method: 'GET', + headers: Object.assign({ + 'User-Agent': this.config.userAgent, + Accept: accept, + }, headers ?? {}), + timeout, + }); + + return await res.text(); + } + + public async getResponse(args: { + url: string, + method: string, + body?: string, + headers: Record, + timeout?: number, + size?: number, + }): Promise { + const timeout = args.timeout ?? 10 * 1000; + + const controller = new AbortController(); + setTimeout(() => { + controller.abort(); + }, timeout * 6); + + const res = await fetch(args.url, { + method: args.method, + headers: args.headers, + body: args.body, + timeout, + size: args.size ?? 10 * 1024 * 1024, + agent: (url) => this.getAgentByUrl(url), + signal: controller.signal, + }); + + if (!res.ok) { + throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); + } + + return res; + } +} diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts new file mode 100644 index 0000000000..b3b0d63627 --- /dev/null +++ b/packages/backend/src/core/IdService.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ulid } from 'ulid'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +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'; + +@Injectable() +export class IdService { + #metohd: string; + + constructor( + @Inject(DI.config) + private config: Config, + ) { + this.#metohd = config.id.toLowerCase(); + } + + public genId(date?: Date): string { + if (!date || (date > new Date())) date = new Date(); + + switch (this.#metohd) { + case 'aid': return genAid(date); + case 'meid': return genMeid(date); + case 'meidg': return genMeidg(date); + case 'ulid': return ulid(date.getTime()); + case 'objectid': return genObjectId(date); + default: throw new Error('unrecognized id generation method'); + } + } +} diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts new file mode 100644 index 0000000000..d215be2131 --- /dev/null +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@nestjs/common'; +import sharp from 'sharp'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; + +export type IImage = { + data: Buffer; + ext: string | null; + type: string; +}; + +@Injectable() +export class ImageProcessingService { + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + /** + * Convert to JPEG + * with resize, remove metadata, resolve orientation, stop animation + */ + public async convertToJpeg(path: string, width: number, height: number): Promise { + return this.convertSharpToJpeg(await sharp(path), width, height); + } + + public async convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { + const data = await sharp + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true, + }) + .rotate() + .jpeg({ + quality: 85, + progressive: true, + }) + .toBuffer(); + + return { + data, + ext: 'jpg', + type: 'image/jpeg', + }; + } + + /** + * Convert to WebP + * with resize, remove metadata, resolve orientation, stop animation + */ + public async convertToWebp(path: string, width: number, height: number, quality = 85): Promise { + return this.convertSharpToWebp(await sharp(path), width, height, quality); + } + + public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise { + const data = await sharp + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true, + }) + .rotate() + .webp({ + quality, + }) + .toBuffer(); + + return { + data, + ext: 'webp', + type: 'image/webp', + }; + } + + /** + * Convert to PNG + * with resize, remove metadata, resolve orientation, stop animation + */ + public async convertToPng(path: string, width: number, height: number): Promise { + return this.convertSharpToPng(await sharp(path), width, height); + } + + public async convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise { + const data = await sharp + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true, + }) + .rotate() + .png() + .toBuffer(); + + return { + data, + ext: 'png', + type: 'image/png', + }; + } +} diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts new file mode 100644 index 0000000000..3a93a49c7b --- /dev/null +++ b/packages/backend/src/core/InstanceActorService.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { UsersRepository } from '@/models/index.js'; +import { Cache } from '@/misc/cache.js'; +import { DI } from '@/di-symbols.js'; +import { CreateSystemUserService } from './CreateSystemUserService.js'; + +const ACTOR_USERNAME = 'instance.actor' as const; + +@Injectable() +export class InstanceActorService { + #cache: Cache; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private createSystemUserService: CreateSystemUserService, + ) { + this.#cache = new Cache(Infinity); + } + + public async getInstanceActor(): Promise { + const cached = this.#cache.get(null); + if (cached) return cached; + + const user = await this.usersRepository.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, + }) as ILocalUser | undefined; + + if (user) { + this.#cache.set(null, user); + return user; + } else { + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as ILocalUser; + this.#cache.set(null, created); + return created; + } + } +} diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts new file mode 100644 index 0000000000..9bc3597baf --- /dev/null +++ b/packages/backend/src/core/InternalStorageService.ts @@ -0,0 +1,45 @@ +import * as fs from 'node:fs'; +import * as Path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const path = Path.resolve(_dirname, '../../../../files'); + +@Injectable() +export class InternalStorageService { + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + public resolvePath(key: string) { + return Path.resolve(path, key); + } + + public read(key: string) { + return fs.createReadStream(this.resolvePath(key)); + } + + public saveFromPath(key: string, srcPath: string) { + fs.mkdirSync(path, { recursive: true }); + fs.copyFileSync(srcPath, this.resolvePath(key)); + return `${this.config.url}/files/${key}`; + } + + public saveFromBuffer(key: string, data: Buffer) { + fs.mkdirSync(path, { recursive: true }); + fs.writeFileSync(this.resolvePath(key), data); + return `${this.config.url}/files/${key}`; + } + + public del(key: string) { + fs.unlink(this.resolvePath(key), () => {}); + } +} diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts new file mode 100644 index 0000000000..669089e1e5 --- /dev/null +++ b/packages/backend/src/core/MessagingService.ts @@ -0,0 +1,300 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User, CacheableUser, IRemoteUser } from '@/models/entities/User.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import { QueueService } from '@/core/QueueService.js'; +import { toArray } from '@/misc/prelude/array.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js'; +import { IdService } from './IdService.js'; +import { GlobalEventService } from './GlobalEventService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js'; +import { PushNotificationService } from './PushNotificationService.js'; + +@Injectable() +export class MessagingService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private userEntityService: UserEntityService, + private messagingMessageEntityService: MessagingMessageEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + private apRendererService: ApRendererService, + private queueService: QueueService, + private pushNotificationService: PushNotificationService, + ) { + } + + 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(), + createdAt: new Date(), + fileId: file ? file.id : null, + recipientId: recipientUser ? recipientUser.id : null, + groupId: recipientGroup ? recipientGroup.id : null, + text: text ? text.trim() : null, + userId: user.id, + isRead: false, + reads: [] as any[], + uri, + } as MessagingMessage; + + await this.messagingMessagesRepository.insert(message); + + const messageObj = await this.messagingMessageEntityService.pack(message); + + if (recipientUser) { + if (this.userEntityService.isLocalUser(user)) { + // 自分のストリーム + this.globalEventService.publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); + this.globalEventService.publishMessagingIndexStream(message.userId, 'message', messageObj); + this.globalEventService.publishMainStream(message.userId, 'messagingMessage', messageObj); + } + + if (this.userEntityService.isLocalUser(recipientUser)) { + // 相手のストリーム + this.globalEventService.publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); + this.globalEventService.publishMessagingIndexStream(recipientUser.id, 'message', messageObj); + this.globalEventService.publishMainStream(recipientUser.id, 'messagingMessage', messageObj); + } + } else if (recipientGroup) { + // グループのストリーム + this.globalEventService.publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); + + // メンバーのストリーム + const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id }); + for (const joining of joinings) { + this.globalEventService.publishMessagingIndexStream(joining.userId, 'message', messageObj); + this.globalEventService.publishMainStream(joining.userId, 'messagingMessage', messageObj); + } + } + + // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する + setTimeout(async () => { + const freshMessage = await this.messagingMessagesRepository.findOneBy({ id: message.id }); + if (freshMessage == null) return; // メッセージが削除されている場合もある + + if (recipientUser && this.userEntityService.isLocalUser(recipientUser)) { + if (freshMessage.isRead) return; // 既読 + + //#region ただしミュートされているなら発行しない + const mute = await this.mutingsRepository.findBy({ + muterId: recipientUser.id, + }); + if (mute.map(m => m.muteeId).includes(user.id)) return; + //#endregion + + this.globalEventService.publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); + this.pushNotificationService.pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj); + } else if (recipientGroup) { + const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id, userId: Not(user.id) }); + for (const joining of joinings) { + if (freshMessage.reads.includes(joining.userId)) return; // 既読 + this.globalEventService.publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); + this.pushNotificationService.pushNotification(joining.userId, 'unreadMessagingMessage', messageObj); + } + } + }, 2000); + + if (recipientUser && this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipientUser)) { + const note = { + id: message.id, + createdAt: message.createdAt, + fileIds: message.fileId ? [message.fileId] : [], + text: message.text, + userId: message.userId, + visibility: 'specified', + mentions: [recipientUser].map(u => u.id), + mentionedRemoteUsers: JSON.stringify([recipientUser].map(u => ({ + uri: u.uri, + username: u.username, + host: u.host, + }))), + } as Note; + + const activity = this.apRendererService.renderActivity(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note)); + + this.queueService.deliver(user, activity, recipientUser.inbox); + } + return messageObj; + } + + public async deleteMessage(message: MessagingMessage) { + await this.messagingMessagesRepository.delete(message.id); + this.#postDeleteMessage(message); + } + + async #postDeleteMessage(message: MessagingMessage) { + if (message.recipientId) { + const user = await this.usersRepository.findOneByOrFail({ id: message.userId }); + const recipient = await this.usersRepository.findOneByOrFail({ id: message.recipientId }); + + if (this.userEntityService.isLocalUser(user)) this.globalEventService.publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); + if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); + + if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) { + const activity = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user)); + this.queueService.deliver(user, activity, recipient.inbox); + } + } else if (message.groupId) { + this.globalEventService.publishGroupMessagingStream(message.groupId, 'deleted', message.id); + } + } + + /** + * Mark messages as read + */ + public async readUserMessagingMessage( + userId: User['id'], + otherpartyId: User['id'], + messageIds: MessagingMessage['id'][], + ) { + if (messageIds.length === 0) return; + + const messages = await this.messagingMessagesRepository.findBy({ + id: In(messageIds), + }); + + for (const message of messages) { + if (message.recipientId !== userId) { + throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); + } + } + + // Update documents + await this.messagingMessagesRepository.update({ + id: In(messageIds), + userId: otherpartyId, + recipientId: userId, + isRead: false, + }, { + isRead: true, + }); + + // Publish event + this.globalEventService.publishMessagingStream(otherpartyId, userId, 'read', messageIds); + this.globalEventService.publishMessagingIndexStream(userId, 'read', messageIds); + + if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages'); + this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのユーザーとのメッセージで未読がなければイベント発行 + const count = await this.messagingMessagesRepository.count({ + where: { + userId: otherpartyId, + recipientId: userId, + isRead: false, + }, + take: 1, + }); + + if (!count) { + this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); + } + } + } + + /** + * Mark messages as read + */ + public async readGroupMessagingMessage( + userId: User['id'], + groupId: UserGroup['id'], + messageIds: MessagingMessage['id'][], + ) { + if (messageIds.length === 0) return; + + // check joined + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userId: userId, + userGroupId: groupId, + }); + + if (joining == null) { + throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); + } + + const messages = await this.messagingMessagesRepository.findBy({ + id: In(messageIds), + }); + + const reads: MessagingMessage['id'][] = []; + + for (const message of messages) { + if (message.userId === userId) continue; + if (message.reads.includes(userId)) continue; + + // Update document + await this.messagingMessagesRepository.createQueryBuilder().update() + .set({ + reads: (() => `array_append("reads", '${joining.userId}')`) as any, + }) + .where('id = :id', { id: message.id }) + .execute(); + + reads.push(message.id); + } + + // Publish event + this.globalEventService.publishGroupMessagingStream(groupId, 'read', { + ids: reads, + userId: userId, + }); + this.globalEventService.publishMessagingIndexStream(userId, 'read', reads); + + if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages'); + this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのグループにおいて未読がなければイベント発行 + const unreadExist = await this.messagingMessagesRepository.createQueryBuilder('message') + .where('message.groupId = :groupId', { groupId: groupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null); + + if (!unreadExist) { + this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); + } + } + } + + 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)); + + if (contents.length > 1) { + const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents); + this.queueService.deliver(user, this.apRendererService.renderActivity(collection), recipient.inbox); + } else { + for (const content of contents) { + this.queueService.deliver(user, this.apRendererService.renderActivity(content), recipient.inbox); + } + } + } +} diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts new file mode 100644 index 0000000000..b5bd423765 --- /dev/null +++ b/packages/backend/src/core/MetaService.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import type { UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { Meta } from '@/models/entities/Meta.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class MetaService implements OnApplicationShutdown { + #cache: Meta | undefined; + #intervalId: NodeJS.Timer; + + constructor( + @Inject(DI.db) + private db: DataSource, + ) { + if (process.env.NODE_ENV !== 'test') { + this.#intervalId = setInterval(() => { + this.fetch(true).then(meta => { + this.#cache = meta; + }); + }, 1000 * 10); + } + } + + async fetch(noCache = false): Promise { + if (!noCache && this.#cache) return this.#cache; + + return await this.db.transaction(async transactionalEntityManager => { + // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する + const metas = await transactionalEntityManager.find(Meta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + this.#cache = meta; + return meta; + } else { + // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う + const saved = await transactionalEntityManager + .upsert( + Meta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); + + this.#cache = saved; + return saved; + } + }); + } + + public onApplicationShutdown(signal?: string | undefined) { + clearInterval(this.#intervalId); + } +} diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts new file mode 100644 index 0000000000..236be4bbf8 --- /dev/null +++ b/packages/backend/src/core/MfmService.ts @@ -0,0 +1,384 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import * as parse5 from 'parse5'; +import { JSDOM } from 'jsdom'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { intersperse } from '@/misc/prelude/array.js'; +import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; +import type * as mfm from 'mfm-js'; + +const treeAdapter = TreeAdapter.defaultTreeAdapter; + +const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; +const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; + +@Injectable() +export class MfmService { + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + 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'); + + const dom = parse5.parseFragment(html); + + let text = ''; + + for (const n of dom.childNodes) { + analyze(n); + } + + return text.trim(); + + function getText(node: TreeAdapter.Node): string { + if (treeAdapter.isTextNode(node)) return node.value; + if (!treeAdapter.isElementNode(node)) return ''; + if (node.nodeName === 'br') return '\n'; + + if (node.childNodes) { + return node.childNodes.map(n => getText(n)).join(''); + } + + return ''; + } + + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { + if (childNodes) { + for (const n of childNodes) { + analyze(n); + } + } + } + + function analyze(node: TreeAdapter.Node) { + if (treeAdapter.isTextNode(node)) { + text += node.value; + return; + } + + // Skip comment or document type node + if (!treeAdapter.isElementNode(node)) return; + + switch (node.nodeName) { + case 'br': { + text += '\n'; + break; + } + + case 'a': + { + const txt = getText(node); + const rel = node.attrs.find(x => x.name === 'rel'); + const href = node.attrs.find(x => x.name === 'href'); + + // ハッシュタグ + if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { + text += txt; + // メンション + } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { + const part = txt.split('@'); + + if (part.length === 2 && href) { + //#region ホスト名部分が省略されているので復元する + const acct = `${txt}@${(new URL(href.value)).hostname}`; + text += acct; + //#endregion + } else if (part.length === 3) { + text += txt; + } + // その他 + } else { + const generateLink = () => { + if (!href && !txt) { + return ''; + } + if (!href) { + return txt; + } + if (!txt || txt === href.value) { // #6383: Missing text node + if (href.value.match(urlRegexFull)) { + return href.value; + } else { + return `<${href.value}>`; + } + } + if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { + return `[${txt}](<${href.value}>)`; // #6846 + } else { + return `[${txt}](${href.value})`; + } + }; + + text += generateLink(); + } + break; + } + + case 'h1': + { + text += '【'; + appendChildren(node.childNodes); + text += '】\n'; + break; + } + + case 'b': + case 'strong': + { + text += '**'; + appendChildren(node.childNodes); + text += '**'; + break; + } + + case 'small': + { + text += ''; + appendChildren(node.childNodes); + text += ''; + break; + } + + case 's': + case 'del': + { + text += '~~'; + appendChildren(node.childNodes); + text += '~~'; + break; + } + + case 'i': + case 'em': + { + text += ''; + appendChildren(node.childNodes); + text += ''; + break; + } + + // block code (
)
+				case 'pre': {
+					if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+						text += '\n```\n';
+						text += getText(node.childNodes[0]);
+						text += '\n```\n';
+					} else {
+						appendChildren(node.childNodes);
+					}
+					break;
+				}
+	
+				// inline code ()
+				case 'code': {
+					text += '`';
+					appendChildren(node.childNodes);
+					text += '`';
+					break;
+				}
+	
+				case 'blockquote': {
+					const t = getText(node);
+					if (t) {
+						text += '\n> ';
+						text += t.split('\n').join('\n> ');
+					}
+					break;
+				}
+	
+				case 'p':
+				case 'h2':
+				case 'h3':
+				case 'h4':
+				case 'h5':
+				case 'h6':
+				{
+					text += '\n\n';
+					appendChildren(node.childNodes);
+					break;
+				}
+	
+				// other block elements
+				case 'div':
+				case 'header':
+				case 'footer':
+				case 'article':
+				case 'li':
+				case 'dt':
+				case 'dd':
+				{
+					text += '\n';
+					appendChildren(node.childNodes);
+					break;
+				}
+	
+				default:	// includes inline elements
+				{
+					appendChildren(node.childNodes);
+					break;
+				}
+			}
+		}
+	}
+
+	public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
+		if (nodes == null) {
+			return null;
+		}
+	
+		const { window } = new JSDOM('');
+	
+		const doc = window.document;
+	
+		function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
+			if (children) {
+				for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
+			}
+		}
+	
+		const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
+			bold: (node) => {
+				const el = doc.createElement('b');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			small: (node) => {
+				const el = doc.createElement('small');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			strike: (node) => {
+				const el = doc.createElement('del');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			italic: (node) => {
+				const el = doc.createElement('i');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			fn: (node) => {
+				const el = doc.createElement('i');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			blockCode: (node) => {
+				const pre = doc.createElement('pre');
+				const inner = doc.createElement('code');
+				inner.textContent = node.props.code;
+				pre.appendChild(inner);
+				return pre;
+			},
+	
+			center: (node) => {
+				const el = doc.createElement('div');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			emojiCode: (node) => {
+				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+			},
+	
+			unicodeEmoji: (node) => {
+				return doc.createTextNode(node.props.emoji);
+			},
+	
+			hashtag: (node) => {
+				const a = doc.createElement('a');
+				a.href = `${this.config.url}/tags/${node.props.hashtag}`;
+				a.textContent = `#${node.props.hashtag}`;
+				a.setAttribute('rel', 'tag');
+				return a;
+			},
+	
+			inlineCode: (node) => {
+				const el = doc.createElement('code');
+				el.textContent = node.props.code;
+				return el;
+			},
+	
+			mathInline: (node) => {
+				const el = doc.createElement('code');
+				el.textContent = node.props.formula;
+				return el;
+			},
+	
+			mathBlock: (node) => {
+				const el = doc.createElement('code');
+				el.textContent = node.props.formula;
+				return el;
+			},
+	
+			link: (node) => {
+				const a = doc.createElement('a');
+				a.href = node.props.url;
+				appendChildren(node.children, a);
+				return a;
+			},
+	
+			mention: (node) => {
+				const a = doc.createElement('a');
+				const { username, host, acct } = node.props;
+				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+				a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`;
+				a.className = 'u-url mention';
+				a.textContent = acct;
+				return a;
+			},
+	
+			quote: (node) => {
+				const el = doc.createElement('blockquote');
+				appendChildren(node.children, el);
+				return el;
+			},
+	
+			text: (node) => {
+				const el = doc.createElement('span');
+				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
+	
+				for (const x of intersperse('br', nodes)) {
+					el.appendChild(x === 'br' ? doc.createElement('br') : x);
+				}
+	
+				return el;
+			},
+	
+			url: (node) => {
+				const a = doc.createElement('a');
+				a.href = node.props.url;
+				a.textContent = node.props.url;
+				return a;
+			},
+	
+			search: (node) => {
+				const a = doc.createElement('a');
+				a.href = `https://www.google.com/search?q=${node.props.query}`;
+				a.textContent = node.props.content;
+				return a;
+			},
+	
+			plain: (node) => {
+				const el = doc.createElement('span');
+				appendChildren(node.children, el);
+				return el;
+			},
+		};
+	
+		appendChildren(nodes, doc.body);
+	
+		return `

${doc.body.innerHTML}

`; + } +} diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts new file mode 100644 index 0000000000..191148ac25 --- /dev/null +++ b/packages/backend/src/core/ModerationLogService.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; + +@Injectable() +export class ModerationLogService { + constructor( + @Inject(DI.moderationLogsRepository) + private moderationLogsRepository: ModerationLogsRepository, + + private idService: IdService, + ) { + } + + public async insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record) { + await this.moderationLogsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: moderator.id, + type: type, + info: info ?? {}, + }); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts new file mode 100644 index 0000000000..83c080893d --- /dev/null +++ b/packages/backend/src/core/NoteCreateService.ts @@ -0,0 +1,742 @@ +import * as mfm from 'mfm-js'; +import { Not, In, DataSource } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { extractMentions } from '@/misc/extract-mentions.js'; +import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; +import { extractHashtags } from '@/misc/extract-hashtags.js'; +import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import { Note } from '@/models/entities/Note.js'; +import { ChannelFollowingsRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { App } from '@/models/entities/App.js'; +import { concat } from '@/misc/prelude/array.js'; +import { IdService } from '@/core/IdService.js'; +import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { IPoll } from '@/models/entities/Poll.js'; +import { Poll } from '@/models/entities/Poll.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { Cache } from '@/misc/cache.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; +import { RelayService } from '@/core/RelayService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { WebhookService } from '@/core/WebhookService.js'; +import { HashtagService } from '@/core/HashtagService.js'; +import { AntennaService } from '@/core/AntennaService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { NoteEntityService } from './entities/NoteEntityService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { NoteReadService } from './NoteReadService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { ResolveUserService } from './remote/ResolveUserService.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; + +const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); + +type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; + +class NotificationManager { + private notifier: { id: User['id']; }; + private note: Note; + private queue: { + target: ILocalUser['id']; + reason: NotificationType; + }[]; + + constructor( + private mutingsRepository: MutingsRepository, + private createNotificationService: CreateNotificationService, + notifier: { id: User['id']; }, + note: Note, + ) { + this.notifier = notifier; + this.note = note; + this.queue = []; + } + + public push(notifiee: ILocalUser['id'], reason: NotificationType) { + // 自分自身へは通知しない + if (this.notifier.id === notifiee) return; + + const exist = this.queue.find(x => x.target === notifiee); + + if (exist) { + // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする + if (reason !== 'mention') { + exist.reason = reason; + } + } else { + this.queue.push({ + reason: reason, + target: notifiee, + }); + } + } + + public async deliver() { + for (const x of this.queue) { + // ミュート情報を取得 + const mentioneeMutes = await this.mutingsRepository.findBy({ + muterId: x.target, + }); + + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); + + // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する + if (!mentioneesMutedUserIds.includes(this.notifier.id)) { + this.createNotificationService.createNotification(x.target, x.reason, { + notifierId: this.notifier.id, + noteId: this.note.id, + }); + } + } + } +} + +type MinimumUser = { + id: User['id']; + host: User['host']; + username: User['username']; + uri: User['uri']; +}; + +type Option = { + createdAt?: Date | null; + name?: string | null; + text?: string | null; + reply?: Note | null; + renote?: Note | null; + files?: DriveFile[] | null; + poll?: IPoll | null; + localOnly?: boolean | null; + cw?: string | null; + visibility?: string; + visibleUsers?: MinimumUser[] | null; + channel?: Channel | null; + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + uri?: string | null; + url?: string | null; + app?: App | null; +}; + +@Injectable() +export class NoteCreateService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + private queueService: QueueService, + private noteReadService: NoteReadService, + private createNotificationService: CreateNotificationService, + private relayService: RelayService, + private federatedInstanceService: FederatedInstanceService, + private hashtagService: HashtagService, + private antennaService: AntennaService, + private webhookService: WebhookService, + private resolveUserService: ResolveUserService, + private apDeliverManagerService: ApDeliverManagerService, + private apRendererService: ApRendererService, + private notesChart: NotesChart, + private perUserNotesChart: PerUserNotesChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + ) {} + + public async create(user: { + id: User['id']; + username: User['username']; + host: User['host']; + isSilenced: User['isSilenced']; + createdAt: User['createdAt']; + }, data: Option, silent = false): Promise { + // チャンネル外にリプライしたら対象のスコープに合わせる + // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) + if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { + if (data.reply.channelId) { + data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId }); + } else { + data.channel = null; + } + } + + // チャンネル内にリプライしたら対象のスコープに合わせる + // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) + if (data.reply && (data.channel == null) && data.reply.channelId) { + data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId }); + } + + if (data.createdAt == null) data.createdAt = new Date(); + if (data.visibility == null) data.visibility = 'public'; + if (data.localOnly == null) data.localOnly = false; + if (data.channel != null) data.visibility = 'public'; + 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'; + } + + // Renote対象が「ホームまたは全体」以外の公開範囲ならreject + if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { + throw new Error('Renote target is not public or home'); + } + + // Renote対象がpublicではないならhomeにする + if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { + data.visibility = 'home'; + } + + // Renote対象がfollowersならfollowersにする + if (data.renote && data.renote.visibility === 'followers') { + data.visibility = 'followers'; + } + + // 返信対象がpublicではないならhomeにする + if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { + data.visibility = 'home'; + } + + // ローカルのみをRenoteしたらローカルのみにする + if (data.renote && data.renote.localOnly && data.channel == null) { + data.localOnly = true; + } + + // ローカルのみにリプライしたらローカルのみにする + if (data.reply && data.reply.localOnly && data.channel == null) { + data.localOnly = true; + } + + if (data.text) { + data.text = data.text.trim(); + } else { + data.text = null; + } + + let tags = data.apHashtags; + let emojis = data.apEmojis; + let mentionedUsers = data.apMentions; + + // Parse MFM if needed + if (!tags || !emojis || !mentionedUsers) { + const tokens = data.text ? mfm.parse(data.text)! : []; + const cwTokens = data.cw ? mfm.parse(data.cw)! : []; + const choiceTokens = data.poll && data.poll.choices + ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) + : []; + + const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); + + tags = data.apHashtags ?? extractHashtags(combinedTokens); + + emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens); + + mentionedUsers = data.apMentions ?? await this.#extractMentionedUsers(user, combinedTokens); + } + + tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); + + if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { + mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId })); + } + + if (data.visibility === 'specified') { + if (data.visibleUsers == null) throw new Error('invalid param'); + + for (const u of data.visibleUsers) { + if (!mentionedUsers.some(x => x.id === u.id)) { + mentionedUsers.push(u); + } + } + + if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { + data.visibleUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId })); + } + } + + const note = await this.#insertNote(user, data, tags, emojis, mentionedUsers); + + setImmediate(() => this.#postNoteCreated(note, user, data, silent, tags!, mentionedUsers!)); + + return note; + } + + 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!), + createdAt: data.createdAt!, + fileIds: data.files ? data.files.map(file => file.id) : [], + replyId: data.reply ? data.reply.id : null, + renoteId: data.renote ? data.renote.id : null, + channelId: data.channel ? data.channel.id : null, + threadId: data.reply + ? data.reply.threadId + ? data.reply.threadId + : data.reply.id + : null, + name: data.name, + text: data.text, + hasPoll: data.poll != null, + cw: data.cw == null ? null : data.cw, + tags: tags.map(tag => normalizeForSearch(tag)), + emojis, + userId: user.id, + localOnly: data.localOnly!, + visibility: data.visibility as any, + visibleUserIds: data.visibility === 'specified' + ? data.visibleUsers + ? data.visibleUsers.map(u => u.id) + : [] + : [], + + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + + // 以下非正規化データ + replyUserId: data.reply ? data.reply.userId : null, + replyUserHost: data.reply ? data.reply.userHost : null, + renoteUserId: data.renote ? data.renote.userId : null, + renoteUserHost: data.renote ? data.renote.userHost : null, + userHost: user.host, + }); + + if (data.uri != null) insert.uri = data.uri; + if (data.url != null) insert.url = data.url; + + // Append mentions data + if (mentionedUsers.length > 0) { + insert.mentions = mentionedUsers.map(u => u.id); + const profiles = await this.userProfilesRepository.findBy({ userId: In(insert.mentions) }); + insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u)).map(u => { + const profile = profiles.find(p => p.userId === u.id); + const url = profile != null ? profile.url : null; + return { + uri: u.uri, + url: url == null ? undefined : url, + username: u.username, + host: u.host, + } as IMentionedRemoteUsers[0]; + })); + } + + // 投稿を作成 + try { + if (insert.hasPoll) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.insert(Note, insert); + + const poll = new Poll({ + noteId: insert.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: insert.visibility, + userId: user.id, + userHost: user.host, + }); + + await transactionalEntityManager.insert(Poll, poll); + }); + } else { + await this.notesRepository.insert(insert); + } + + return insert; + } catch (e) { + // duplicate key error + if (isDuplicateKeyValueError(e)) { + const err = new Error('Duplicated note'); + err.name = 'duplicated'; + throw err; + } + + console.error(e); + + throw e; + } + } + + async #postNoteCreated(note: Note, user: { + id: User['id']; + username: User['username']; + host: User['host']; + isSilenced: User['isSilenced']; + createdAt: User['createdAt']; + }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { + // 統計を更新 + this.notesChart.update(note, true); + this.perUserNotesChart.update(user, note, true); + + // Register host + if (this.userEntityService.isRemoteUser(user)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => { + this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); + this.instanceChart.updateNote(i.host, note, true); + }); + } + + // ハッシュタグ更新 + if (data.visibility === 'public' || data.visibility === 'home') { + this.hashtagService.updateHashtags(user, tags); + } + + // Increment notes count (user) + this.#incNotesCountOfUser(user); + + // Word mute + mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({ + where: { + enableWordMute: true, + }, + select: ['userId', 'mutedWords'], + })).then(us => { + for (const u of us) { + checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { + if (shouldMute) { + this.mutedNotesRepository.insert({ + id: this.idService.genId(), + userId: u.userId, + noteId: note.id, + reason: 'word', + }); + } + }); + } + }); + + // Antenna + for (const antenna of (await this.antennaService.getAntennas())) { + this.antennaService.checkHitAntenna(antenna, note, user).then(hit => { + if (hit) { + this.antennaService.addNoteToAntenna(antenna, note, user); + } + }); + } + + // Channel + if (note.channelId) { + this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => { + for (const following of followings) { + this.noteReadService.insertNoteUnread(following.followerId, note, { + isSpecified: false, + isMentioned: false, + }); + } + }); + } + + if (data.reply) { + this.#saveReply(data.reply, note); + } + + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { + this.#incRenoteCount(data.renote); + } + + if (data.poll && data.poll.expiresAt) { + const delay = data.poll.expiresAt.getTime() - Date.now(); + this.queueService.endedPollNotificationQueue.add({ + noteId: note.id, + }, { + delay, + removeOnComplete: true, + }); + } + + if (!silent) { + if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); + + // 未読通知を作成 + if (data.visibility === 'specified') { + if (data.visibleUsers == null) throw new Error('invalid param'); + + for (const u of data.visibleUsers) { + // ローカルユーザーのみ + if (!this.userEntityService.isLocalUser(u)) continue; + + this.noteReadService.insertNoteUnread(u.id, note, { + isSpecified: true, + isMentioned: false, + }); + } + } else { + for (const u of mentionedUsers) { + // ローカルユーザーのみ + if (!this.userEntityService.isLocalUser(u)) continue; + + this.noteReadService.insertNoteUnread(u.id, note, { + isSpecified: false, + isMentioned: true, + }); + } + } + + // Pack the note + const noteObj = await this.noteEntityService.pack(note); + + this.globalEventServie.publishNotesStream(noteObj); + + this.webhookService.getActiveWebhooks().then(webhooks => { + webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'note', { + note: noteObj, + }); + } + }); + + const nm = new NotificationManager(this.mutingsRepository, this.createNotificationService, user, note); + const nmRelatedPromises = []; + + await this.#createMentionedEvents(mentionedUsers, note, nm); + + // If has in reply to note + if (data.reply) { + // 通知 + if (data.reply.userHost === null) { + const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ + userId: data.reply.userId, + threadId: data.reply.threadId ?? data.reply.id, + }); + + if (!threadMuted) { + nm.push(data.reply.userId, 'reply'); + this.globalEventServie.publishMainStream(data.reply.userId, 'reply', noteObj); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'reply', { + note: noteObj, + }); + } + } + } + } + + // If it is renote + if (data.renote) { + const type = data.text ? 'quote' : 'renote'; + + // Notify + if (data.renote.userHost === null) { + nm.push(data.renote.userId, type); + } + + // Publish event + if ((user.id !== data.renote.userId) && data.renote.userHost === null) { + this.globalEventServie.publishMainStream(data.renote.userId, 'renote', noteObj); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'renote', { + note: noteObj, + }); + } + } + } + + Promise.all(nmRelatedPromises).then(() => { + nm.deliver(); + }); + + //#region AP deliver + if (this.userEntityService.isLocalUser(user)) { + (async () => { + const noteActivity = await this.#renderNoteOrRenoteActivity(data, note); + const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); + + // メンションされたリモートユーザーに配送 + for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { + dm.addDirectRecipe(u as IRemoteUser); + } + + // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + if (data.reply && data.reply.userHost !== null) { + const u = await this.usersRepository.findOneBy({ id: data.reply.userId }); + if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + if (data.renote && data.renote.userHost !== null) { + const u = await this.usersRepository.findOneBy({ id: data.renote.userId }); + if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // フォロワーに配送 + if (['public', 'home', 'followers'].includes(note.visibility)) { + dm.addFollowersRecipe(); + } + + if (['public'].includes(note.visibility)) { + this.relayService.deliverToRelays(user, noteActivity); + } + + dm.execute(); + })(); + } + //#endregion + } + + if (data.channel) { + this.channelsRepository.increment({ id: data.channel.id }, 'notesCount', 1); + this.channelsRepository.update(data.channel.id, { + lastNotedAt: new Date(), + }); + + this.notesRepository.countBy({ + userId: user.id, + channelId: data.channel.id, + }).then(count => { + // この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる + // TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい + if (count === 1) { + this.channelsRepository.increment({ id: data.channel!.id }, 'usersCount', 1); + } + }); + } + + // Register to search database + this.#index(note); + } + + #incRenoteCount(renote: Note) { + this.notesRepository.createQueryBuilder().update() + .set({ + renoteCount: () => '"renoteCount" + 1', + score: () => '"score" + 1', + }) + .where('id = :id', { id: renote.id }) + .execute(); + } + + 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({ + userId: u.id, + threadId: note.threadId ?? note.id, + }); + + if (threadMuted) { + continue; + } + + const detailPackedNote = await this.noteEntityService.pack(note, u, { + detail: true, + }); + + this.globalEventServie.publishMainStream(u.id, 'mention', detailPackedNote); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'mention', { + note: detailPackedNote, + }); + } + + // Create notification + nm.push(u.id, 'mention'); + } + } + + #saveReply(reply: Note, note: Note) { + this.notesRepository.increment({ id: reply.id }, 'repliesCount', 1); + } + + async #renderNoteOrRenoteActivity(data: Option, note: Note) { + if (data.localOnly) return null; + + const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) + ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) + : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); + + return this.apRendererService.renderActivity(content); + } + + #index(note: Note) { + if (note.text == null || this.config.elasticsearch == null) return; + /* + es!.index({ + index: this.config.elasticsearch.index ?? 'misskey_note', + id: note.id.toString(), + body: { + text: normalizeForSearch(note.text), + userId: note.userId, + userHost: note.userHost, + }, + });*/ + } + + #incNotesCountOfUser(user: { id: User['id']; }) { + this.usersRepository.createQueryBuilder().update() + .set({ + updatedAt: new Date(), + notesCount: () => '"notesCount" + 1', + }) + .where('id = :id', { id: user.id }) + .execute(); + } + + async #extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { + if (tokens == null) return []; + + const mentions = extractMentions(tokens); + let mentionedUsers = (await Promise.all(mentions.map(m => + this.resolveUserService.resolveUser(m.username, m.host ?? user.host).catch(() => null), + ))).filter(x => x != null) as User[]; + + // Drop duplicate users + mentionedUsers = mentionedUsers.filter((u, i, self) => + i === self.findIndex(u2 => u.id === u2.id), + ); + + return mentionedUsers; + } +} diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts new file mode 100644 index 0000000000..9153418beb --- /dev/null +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -0,0 +1,168 @@ +import { Brackets, In } from 'typeorm'; +import { Injectable, Inject } from '@nestjs/common'; +import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; +import { RelayService } from '@/core/RelayService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; + +@Injectable() +export class NoteDeleteService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private userEntityService: UserEntityService, + private globalEventServie: GlobalEventService, + private relayService: RelayService, + private federatedInstanceService: FederatedInstanceService, + private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, + private notesChart: NotesChart, + private perUserNotesChart: PerUserNotesChart, + private instanceChart: InstanceChart, + ) {} + + /** + * 投稿を削除します。 + * @param user 投稿者 + * @param note 投稿 + */ + async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false) { + const deletedAt = new Date(); + + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { + this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1); + this.notesRepository.decrement({ id: note.renoteId }, 'score', 1); + } + + if (note.replyId) { + await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); + } + + if (!quiet) { + this.globalEventServie.publishNoteStream(note.id, 'deleted', { + deletedAt: deletedAt, + }); + + //#region ローカルの投稿なら削除アクティビティを配送 + if (this.userEntityService.isLocalUser(user) && !note.localOnly) { + let renote: Note | null = null; + + // if deletd note is renote + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { + renote = await this.notesRepository.findOneBy({ + id: note.renoteId, + }); + } + + const content = this.apRendererService.renderActivity(renote + ? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user) + : this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user)); + + this.#deliverToConcerned(user, note, content); + } + + // also deliever delete activity to cascaded notes + const cascadingNotes = (await this.#findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes + for (const cascadingNote of cascadingNotes) { + if (!cascadingNote.user) continue; + if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; + const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); + this.#deliverToConcerned(cascadingNote.user, cascadingNote, content); + } + //#endregion + + // 統計を更新 + this.notesChart.update(note, false); + this.perUserNotesChart.update(user, note, false); + + if (this.userEntityService.isRemoteUser(user)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => { + this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); + this.instanceChart.updateNote(i.host, note, false); + }); + } + } + + await this.notesRepository.delete({ + id: note.id, + userId: user.id, + }); + } + + async #findCascadingNotes(note: Note) { + const cascadingNotes: Note[] = []; + + const recursive = async (noteId: string) => { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.replyId = :noteId', { noteId }) + .orWhere(new Brackets(q => { + q.where('note.renoteId = :noteId', { noteId }) + .andWhere('note.text IS NOT NULL'); + })) + .leftJoinAndSelect('note.user', 'user'); + const replies = await query.getMany(); + for (const reply of replies) { + cascadingNotes.push(reply); + await recursive(reply.id); + } + }; + await recursive(note.id); + + return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users + } + + async #getMentionedRemoteUsers(note: Note) { + const where = [] as any[]; + + // mention / reply / dm + const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + if (uris.length > 0) { + where.push( + { uri: In(uris) }, + ); + } + + // renote / quote + if (note.renoteUserId) { + where.push({ + id: note.renoteUserId, + }); + } + + if (where.length === 0) return []; + + return await this.usersRepository.find({ + where, + }) as IRemoteUser[]; + } + + async #deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); + const remoteUsers = await this.#getMentionedRemoteUsers(note); + for (const remoteUser of remoteUsers) { + this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + } + } +} diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts new file mode 100644 index 0000000000..576e90bd43 --- /dev/null +++ b/packages/backend/src/core/NotePiningService.ts @@ -0,0 +1,117 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { RelayService } from '@/core/RelayService.js'; +import { Config } from '@/config.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; + +@Injectable() +export class NotePiningService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private relayService: RelayService, + private apDeliverManagerService: ApDeliverManagerService, + private apRendererService: ApRendererService, + ) { + } + + /** + * 指定した投稿をピン留めします + * @param user + * @param noteId + */ + public async addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { + // Fetch pinee + const note = await this.notesRepository.findOneBy({ + id: noteId, + userId: user.id, + }); + + if (note == null) { + throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); + } + + const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id }); + + if (pinings.length >= 5) { + throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); + } + + if (pinings.some(pining => pining.noteId === note.id)) { + throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); + } + + await this.userNotePiningsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id, + } as UserNotePining); + + // Deliver to remote followers + if (this.userEntityService.isLocalUser(user)) { + this.deliverPinnedChange(user.id, note.id, true); + } + } + + /** + * 指定した投稿のピン留めを解除します + * @param user + * @param noteId + */ + public async removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { + // Fetch unpinee + const note = await this.notesRepository.findOneBy({ + id: noteId, + userId: user.id, + }); + + if (note == null) { + throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); + } + + this.userNotePiningsRepository.delete({ + userId: user.id, + noteId: note.id, + }); + + // Deliver to remote followers + if (this.userEntityService.isLocalUser(user)) { + this.deliverPinnedChange(user.id, noteId, false); + } + } + + 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'); + + if (!this.userEntityService.isLocalUser(user)) return; + + const target = `${this.config.url}/users/${user.id}/collections/featured`; + const item = `${this.config.url}/notes/${noteId}`; + const content = this.apRendererService.renderActivity(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); + + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); + } +} diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts new file mode 100644 index 0000000000..b1572c631a --- /dev/null +++ b/packages/backend/src/core/NoteReadService.ts @@ -0,0 +1,214 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, IsNull, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/entities/User.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Note } from '@/models/entities/Note.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { NotificationService } from './NotificationService.js'; +import { AntennaService } from './AntennaService.js'; + +@Injectable() +export class NoteReadService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + private notificationService: NotificationService, + private antennaService: AntennaService, + ) { + } + + public async insertNoteUnread(userId: User['id'], note: Note, params: { + // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse + isSpecified: boolean; + isMentioned: boolean; + }): Promise { + //#region ミュートしているなら無視 + // TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする + const mute = await this.mutingsRepository.findBy({ + muterId: userId, + }); + if (mute.map(m => m.muteeId).includes(note.userId)) return; + //#endregion + + // スレッドミュート + const threadMute = await this.noteThreadMutingsRepository.findOneBy({ + userId: userId, + threadId: note.threadId ?? note.id, + }); + if (threadMute) return; + + const unread = { + id: this.idService.genId(), + noteId: note.id, + userId: userId, + isSpecified: params.isSpecified, + isMentioned: params.isMentioned, + noteChannelId: note.channelId, + noteUserId: note.userId, + }; + + await this.noteUnreadsRepository.insert(unread); + + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する + setTimeout(async () => { + const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); + + if (exist == null) return; + + if (params.isMentioned) { + this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); + } + if (params.isSpecified) { + this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); + } + if (note.channelId) { + this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); + } + }, 2000); + } + + public async read( + userId: User['id'], + notes: (Note | Packed<'Note'>)[], + info?: { + following: Set; + followingChannels: Set; + }, + ): Promise { + const following = info?.following ? info.following : new Set((await this.followingsRepository.find({ + where: { + followerId: userId, + }, + select: ['followeeId'], + })).map(x => x.followeeId)); + const followingChannels = info?.followingChannels ? info.followingChannels : new Set((await this.channelFollowingsRepository.find({ + where: { + followerId: userId, + }, + select: ['followeeId'], + })).map(x => x.followeeId)); + + const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); + const readMentions: (Note | Packed<'Note'>)[] = []; + const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; + const readChannelNotes: (Note | Packed<'Note'>)[] = []; + const readAntennaNotes: (Note | Packed<'Note'>)[] = []; + + for (const note of notes) { + if (note.mentions && note.mentions.includes(userId)) { + readMentions.push(note); + } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { + readSpecifiedNotes.push(note); + } + + if (note.channelId && followingChannels.has(note.channelId)) { + readChannelNotes.push(note); + } + + if (note.user != null) { // たぶんnullになることは無いはずだけど一応 + for (const antenna of myAntennas) { + if (await this.antennaService.checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) { + readAntennaNotes.push(note); + } + } + } + } + + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { + // Remove the record + await this.noteUnreadsRepository.delete({ + userId: userId, + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), + }); + + // TODO: ↓まとめてクエリしたい + + this.noteUnreadsRepository.countBy({ + userId: userId, + isMentioned: true, + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + isSpecified: true, + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + noteChannelId: Not(IsNull()), + }).then(channelNoteCount => { + if (channelNoteCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllChannels'); + } + }); + + this.notificationService.readNotificationByQuery(userId, { + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), + }); + } + + if (readAntennaNotes.length > 0) { + await this.antennaNotesRepository.update({ + antennaId: In(myAntennas.map(a => a.id)), + noteId: In(readAntennaNotes.map(n => n.id)), + }, { + read: true, + }); + + // TODO: まとめてクエリしたい + for (const antenna of myAntennas) { + const count = await this.antennaNotesRepository.countBy({ + antennaId: antenna.id, + read: false, + }); + + if (count === 0) { + this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); + } + } + + this.userEntityService.getHasUnreadAntenna(userId).then(unread => { + if (!unread) { + this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); + } + }); + } + } +} diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts new file mode 100644 index 0000000000..44957d62d3 --- /dev/null +++ b/packages/backend/src/core/NotificationService.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { Notification } from '@/models/entities/Notification.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { GlobalEventService } from './GlobalEventService.js'; +import { PushNotificationService } from './PushNotificationService.js'; + +@Injectable() +export class NotificationService { + constructor( + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private pushNotificationService: PushNotificationService, + ) { + } + + public async readNotification( + userId: User['id'], + notificationIds: Notification['id'][], + ) { + if (notificationIds.length === 0) return; + + // Update documents + const result = await this.notificationsRepository.update({ + notifieeId: userId, + id: In(notificationIds), + isRead: false, + }, { + isRead: true, + }); + + if (result.affected === 0) return; + + if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.#postReadAllNotifications(userId); + else return this.#postReadNotifications(userId, notificationIds); + } + + public async readNotificationByQuery( + userId: User['id'], + query: Record, + ) { + const notificationIds = await this.notificationsRepository.findBy({ + ...query, + notifieeId: userId, + isRead: false, + }).then(notifications => notifications.map(notification => notification.id)); + + return this.readNotification(userId, notificationIds); + } + + #postReadAllNotifications(userId: User['id']) { + this.globalEventService.publishMainStream(userId, 'readAllNotifications'); + return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined); + } + + #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 new file mode 100644 index 0000000000..8bc94c8a82 --- /dev/null +++ b/packages/backend/src/core/PollService.ts @@ -0,0 +1,115 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; +import { RelayService } from '@/core/RelayService.js'; +import type { CacheableUser } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; + +@Injectable() +export class PollService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private relayService: RelayService, + private globalEventServie: GlobalEventService, + private createNotificationService: CreateNotificationService, + private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, + ) { + } + + public async vote(user: CacheableUser, note: Note, choice: number) { + const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); + + if (poll == null) throw new Error('poll not found'); + + // Check whether is valid choice + if (poll.choices[choice] == null) throw new Error('invalid choice param'); + + // Check blocking + if (note.userId !== user.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: note.userId, + blockeeId: user.id, + }); + if (block) { + throw new Error('blocked'); + } + } + + // if already voted + const exist = await this.pollVotesRepository.findBy({ + noteId: note.id, + userId: user.id, + }); + + if (poll.multiple) { + if (exist.some(x => x.choice === choice)) { + throw new Error('already voted'); + } + } else if (exist.length !== 0) { + throw new Error('already voted'); + } + + // Create vote + await this.pollVotesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + noteId: note.id, + userId: user.id, + choice: choice, + }); + + // Increment votes count + const index = choice + 1; // In SQL, array index is 1 based + await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); + + this.globalEventServie.publishNoteStream(note.id, 'pollVoted', { + choice: choice, + userId: user.id, + }); + + // Notify + this.createNotificationService.createNotification(note.userId, 'pollVote', { + notifierId: user.id, + noteId: note.id, + choice: choice, + }); + } + + public async deliverQuestionUpdate(noteId: Note['id']) { + const note = await this.notesRepository.findOneBy({ id: noteId }); + if (note == null) throw new Error('note not found'); + + const user = await this.usersRepository.findOneBy({ id: note.userId }); + if (user == null) throw new Error('note not found'); + + if (this.userEntityService.isLocalUser(user)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); + } + } +} diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts new file mode 100644 index 0000000000..40ccc8226a --- /dev/null +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import type { ILocalUser, User } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; +import { MetaService } from './MetaService.js'; + +@Injectable() +export class ProxyAccountService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private metaService: MetaService, + ) { + } + + public async fetch(): Promise { + const meta = await this.metaService.fetch(); + if (meta.proxyAccountId == null) return null; + return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser; + } +} diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts new file mode 100644 index 0000000000..31d29bed97 --- /dev/null +++ b/packages/backend/src/core/PushNotificationService.ts @@ -0,0 +1,101 @@ +import { Inject, Injectable } from '@nestjs/common'; +import push from 'web-push'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { Packed } from '@/misc/schema'; +import { getNoteSummary } from '@/misc/get-note-summary.js'; +import { SwSubscriptionsRepository } from '@/models/index.js'; +import { MetaService } from './MetaService.js'; + +// Defined also packages/sw/types.ts#L14-L21 +type pushNotificationsTypes = { + 'notification': Packed<'Notification'>; + 'unreadMessagingMessage': Packed<'MessagingMessage'>; + 'readNotifications': { notificationIds: string[] }; + 'readAllNotifications': undefined; + 'readAllMessagingMessages': undefined; + 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; +}; + +// プッシュメッセージサーバーには文字数制限があるため、内容を削減します +function truncateNotification(notification: Packed<'Notification'>): any { + if (notification.note) { + return { + ...notification, + note: { + ...notification.note, + // textをgetNoteSummaryしたものに置き換える + text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), + + cw: undefined, + reply: undefined, + renote: undefined, + user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる + }, + }; + } + + return notification; +} + +@Injectable() +export class PushNotificationService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.swSubscriptionsRepository) + private swSubscriptionsRepository: SwSubscriptionsRepository, + + private metaService: MetaService, + ) { + } + + public async pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { + const meta = await this.metaService.fetch(); + + if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; + + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 + push.setVapidDetails(this.config.url, + meta.swPublicKey, + meta.swPrivateKey); + + // Fetch + const subscriptions = await this.swSubscriptionsRepository.findBy({ + userId: userId, + }); + + for (const subscription of subscriptions) { + const pushSubscription = { + endpoint: subscription.endpoint, + keys: { + auth: subscription.auth, + p256dh: subscription.publickey, + }, + }; + + push.sendNotification(pushSubscription, JSON.stringify({ + type, + body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body, + userId, + dateTime: (new Date()).getTime(), + }), { + proxy: this.config.proxy, + }).catch((err: any) => { + //swLogger.info(err.statusCode); + //swLogger.info(err.headers); + //swLogger.info(err.body); + + if (err.statusCode === 410) { + this.swSubscriptionsRepository.delete({ + userId: userId, + endpoint: subscription.endpoint, + auth: subscription.auth, + publickey: subscription.publickey, + }); + } + }); + } + } +} diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts new file mode 100644 index 0000000000..1613f70c80 --- /dev/null +++ b/packages/backend/src/core/QueryService.ts @@ -0,0 +1,262 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/entities/User.js'; +import type { SelectQueryBuilder } from 'typeorm'; + +@Injectable() +export class QueryService { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + ) { + } + + public makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder { + if (sinceId && untilId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.id`, 'ASC'); + } else if (untilId) { + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceDate && untilDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else if (sinceDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.orderBy(`${q.alias}.createdAt`, 'ASC'); + } else if (untilDate) { + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else { + q.orderBy(`${q.alias}.id`, 'DESC'); + } + return q; + } + + // ここでいうBlockedは被Blockedの意 + public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') + .select('blocking.blockerId') + .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); + + // 投稿の作者にブロックされていない かつ + // 投稿の返信先の作者にブロックされていない かつ + // 投稿の引用元の作者にブロックされていない + q + .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); + })); + + q.setParameters(blockingQuery.getParameters()); + } + + public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') + .select('blocking.blockeeId') + .where('blocking.blockerId = :blockerId', { blockerId: me.id }); + + const blockedQuery = this.blockingsRepository.createQueryBuilder('blocking') + .select('blocking.blockerId') + .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); + + q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`); + q.setParameters(blockingQuery.getParameters()); + + q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`); + q.setParameters(blockedQuery.getParameters()); + } + + public generateChannelQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { + if (me == null) { + q.andWhere('note.channelId IS NULL'); + } else { + q.leftJoinAndSelect('note.channel', 'channel'); + + const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') + .select('channelFollowing.followeeId') + .where('channelFollowing.followerId = :followerId', { followerId: me.id }); + + q.andWhere(new Brackets(qb => { qb + // チャンネルのノートではない + .where('note.channelId IS NULL') + // または自分がフォローしているチャンネルのノート + .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); + })); + + q.setParameters(channelFollowingQuery.getParameters()); + } + } + + public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') + .select('muted.noteId') + .where('muted.userId = :userId', { userId: me.id }); + + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); + + q.setParameters(mutedQuery.getParameters()); + } + + public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') + .select('threadMuted.threadId') + .where('threadMuted.userId = :userId', { userId: me.id }); + + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); + q.andWhere(new Brackets(qb => { qb + .where('note.threadId IS NULL') + .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); + })); + + q.setParameters(mutedQuery.getParameters()); + } + + public generateMutedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }, exclude?: User): void { + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + if (exclude) { + mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); + } + + const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: me.id }); + + // 投稿の作者をミュートしていない かつ + // 投稿の返信先の作者をミュートしていない かつ + // 投稿の引用元の作者をミュートしていない + q + .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + // mute instances + .andWhere(new Brackets(qb => { qb + .andWhere('note.userHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); + })); + + q.setParameters(mutingQuery.getParameters()); + q.setParameters(mutingInstanceQuery.getParameters()); + } + + public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); + + q.setParameters(mutingQuery.getParameters()); + } + + public generateRepliesQuery(q: SelectQueryBuilder, me?: Pick | null): void { + if (me == null) { + q.andWhere(new Brackets(qb => { qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } else if (!me.showTimelineReplies) { + q.andWhere(new Brackets(qb => { qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 + .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.userId = :meId', { meId: me.id }); + })) + .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } + } + + 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) { + q.andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })); + } else { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :meId'); + + q.andWhere(new Brackets(qb => { qb + // 公開投稿である + .where(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + // または 自分自身 + .orWhere('note.userId = :meId') + // または 自分宛て + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') + .orWhere(new Brackets(qb => { qb + // または フォロワー宛ての投稿であり、 + .where('note.visibility = \'followers\'') + .andWhere(new Brackets(qb => { qb + // 自分がフォロワーである + .where(`note.userId IN (${ followingQuery.getQuery() })`) + // または 自分の投稿へのリプライ + .orWhere('note.replyUserId = :meId'); + })); + })); + })); + + q.setParameters({ meId: me.id }); + } + } +} + diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts new file mode 100644 index 0000000000..7e771c100f --- /dev/null +++ b/packages/backend/src/core/QueueService.ts @@ -0,0 +1,242 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; +import type { IActivity } from '@/core/remote/activitypub/type.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './queue/QueueModule.js'; +import type { ThinUser } from '../queue/types.js'; +import type httpSignature from '@peertube/http-signature'; + +@Injectable() +export class QueueService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject('queue:system') public systemQueue: SystemQueue, + @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:deliver') public deliverQueue: DeliverQueue, + @Inject('queue:inbox') public inboxQueue: InboxQueue, + @Inject('queue:db') public dbQueue: DbQueue, + @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + ) {} + + public deliver(user: ThinUser, content: IActivity, to: string | null) { + if (content == null) return null; + if (to == null) return null; + + const data = { + user: { + id: user.id, + }, + content, + to, + }; + + return this.deliverQueue.add(data, { + attempts: this.config.deliverJobMaxAttempts ?? 12, + timeout: 1 * 60 * 1000, // 1min + backoff: { + type: 'apBackoff', + }, + removeOnComplete: true, + removeOnFail: true, + }); + } + + public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { + const data = { + activity: activity, + signature, + }; + + return this.inboxQueue.add(data, { + attempts: this.config.inboxJobMaxAttempts ?? 8, + timeout: 5 * 60 * 1000, // 5min + backoff: { + type: 'apBackoff', + }, + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createDeleteDriveFilesJob(user: ThinUser) { + return this.dbQueue.add('deleteDriveFiles', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportCustomEmojisJob(user: ThinUser) { + return this.dbQueue.add('exportCustomEmojis', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportNotesJob(user: ThinUser) { + return this.dbQueue.add('exportNotes', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { + return this.dbQueue.add('exportFollowing', { + user: user, + excludeMuting, + excludeInactive, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportMuteJob(user: ThinUser) { + return this.dbQueue.add('exportMuting', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportBlockingJob(user: ThinUser) { + return this.dbQueue.add('exportBlocking', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createExportUserListsJob(user: ThinUser) { + return this.dbQueue.add('exportUserLists', { + user: user, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { + return this.dbQueue.add('importFollowing', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { + return this.dbQueue.add('importMuting', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { + return this.dbQueue.add('importBlocking', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { + return this.dbQueue.add('importUserLists', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { + return this.dbQueue.add('importCustomEmojis', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { + return this.dbQueue.add('deleteAccount', { + user: user, + soft: opts.soft, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createDeleteObjectStorageFileJob(key: string) { + return this.objectStorageQueue.add('deleteFile', { + key: key, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public createCleanRemoteFilesJob() { + return this.objectStorageQueue.add('cleanRemoteFiles', {}, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + public webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { + const data = { + type, + content, + webhookId: webhook.id, + userId: webhook.userId, + to: webhook.url, + secret: webhook.secret, + createdAt: Date.now(), + eventId: uuid(), + }; + + return this.webhookDeliverQueue.add(data, { + attempts: 4, + timeout: 1 * 60 * 1000, // 1min + backoff: { + type: 'apBackoff', + }, + removeOnComplete: true, + removeOnFail: true, + }); + } + + public destroy() { + this.deliverQueue.once('cleaned', (jobs, status) => { + //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); + }); + this.deliverQueue.clean(0, 'delayed'); + + this.inboxQueue.once('cleaned', (jobs, status) => { + //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); + }); + this.inboxQueue.clean(0, 'delayed'); + } +} diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts new file mode 100644 index 0000000000..3006456577 --- /dev/null +++ b/packages/backend/src/core/ReactionService.ts @@ -0,0 +1,340 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import { IdService } from '@/core/IdService.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; +import { emojiRegex } from '@/misc/emoji-regex.js'; +import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; +import { NoteEntityService } from './entities/NoteEntityService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { MetaService } from './MetaService.js'; +import { UtilityService } from './UtilityService.js'; + +const legacies: Record = { + 'like': '👍', + 'love': '❤', // ここに記述する場合は異体字セレクタを入れない + 'laugh': '😆', + 'hmm': '🤔', + 'surprise': '😮', + 'congrats': '🎉', + 'angry': '💢', + 'confused': '😥', + 'rip': '😇', + 'pudding': '🍮', + 'star': '⭐', +}; + +type DecodedReaction = { + /** + * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') + */ + reaction: string; + + /** + * name (カスタム絵文字の場合name, Emojiクエリに使う) + */ + name?: string; + + /** + * host (カスタム絵文字の場合host, Emojiクエリに使う) + */ + host?: string | null; +}; + +@Injectable() +export class ReactionService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private utilityService: UtilityService, + private metaService: MetaService, + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, + private createNotificationService: CreateNotificationService, + private perUserReactionsChart: PerUserReactionsChart, + ) { + } + + public async create(user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) { + // Check blocking + if (note.userId !== user.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: note.userId, + blockeeId: user.id, + }); + if (block) { + throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); + } + } + + // check visibility + if (!await this.noteEntityService.isVisibleForMe(note, user.id)) { + throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); + } + + // TODO: cache + reaction = await this.toDbReaction(reaction, user.host); + + const record: NoteReaction = { + id: this.idService.genId(), + createdAt: new Date(), + noteId: note.id, + userId: user.id, + reaction, + }; + + // Create reaction + try { + await this.noteReactionsRepository.insert(record); + } catch (e) { + if (isDuplicateKeyValueError(e)) { + const exists = await this.noteReactionsRepository.findOneByOrFail({ + noteId: note.id, + userId: user.id, + }); + + if (exists.reaction !== reaction) { + // 別のリアクションがすでにされていたら置き換える + await this.delete(user, note); + await this.noteReactionsRepository.insert(record); + } else { + // 同じリアクションがすでにされていたらエラー + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } + } else { + throw e; + } + } + + // Increment reactions count + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + score: () => '"score" + 1', + }) + .where('id = :id', { id: note.id }) + .execute(); + + this.perUserReactionsChart.update(user, note); + + // カスタム絵文字リアクションだったら絵文字情報も送る + const decodedReaction = this.decodeReaction(reaction); + + const emoji = await this.emojisRepository.findOne({ + where: { + name: decodedReaction.name, + host: decodedReaction.host ?? IsNull(), + }, + select: ['name', 'host', 'originalUrl', 'publicUrl'], + }); + + this.globalEventServie.publishNoteStream(note.id, 'reacted', { + reaction: decodedReaction.reaction, + emoji: emoji != null ? { + name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, + url: emoji.publicUrl ?? emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため + } : null, + userId: user.id, + }); + + // リアクションされたユーザーがローカルユーザーなら通知を作成 + if (note.userHost === null) { + this.createNotificationService.createNotification(note.userId, 'reaction', { + notifierId: user.id, + noteId: note.id, + reaction: reaction, + }); + } + + //#region 配信 + if (this.userEntityService.isLocalUser(user) && !note.localOnly) { + const content = this.apRendererService.renderActivity(await this.apRendererService.renderLike(record, note)); + const dm = this.apDeliverManagerService.createDeliverManager(user, content); + if (note.userHost !== null) { + const reactee = await this.usersRepository.findOneBy({ id: note.userId }); + dm.addDirectRecipe(reactee as IRemoteUser); + } + + if (['public', 'home', 'followers'].includes(note.visibility)) { + dm.addFollowersRecipe(); + } else if (note.visibility === 'specified') { + const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id }))); + for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) { + dm.addDirectRecipe(u as IRemoteUser); + } + } + + dm.execute(); + } + //#endregion + } + + public async delete(user: { id: User['id']; host: User['host']; }, note: Note) { + // if already unreacted + const exist = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + }); + + if (exist == null) { + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + } + + // Delete reaction + const result = await this.noteReactionsRepository.delete(exist.id); + + if (result.affected !== 1) { + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + } + + // Decrement reactions count + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + + this.notesRepository.decrement({ id: note.id }, 'score', 1); + + this.globalEventServie.publishNoteStream(note.id, 'unreacted', { + reaction: this.decodeReaction(exist.reaction).reaction, + userId: user.id, + }); + + //#region 配信 + if (this.userEntityService.isLocalUser(user) && !note.localOnly) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); + const dm = this.apDeliverManagerService.createDeliverManager(user, content); + if (note.userHost !== null) { + const reactee = await this.usersRepository.findOneBy({ id: note.userId }); + dm.addDirectRecipe(reactee as IRemoteUser); + } + dm.addFollowersRecipe(); + dm.execute(); + } + //#endregion + } + + public async getFallbackReaction(): Promise { + const meta = await this.metaService.fetch(); + return meta.useStarForReactionFallback ? '⭐' : '👍'; + } + + public convertLegacyReactions(reactions: Record) { + const _reactions = {} as Record; + + for (const reaction of Object.keys(reactions)) { + if (reactions[reaction] <= 0) continue; + + if (Object.keys(legacies).includes(reaction)) { + if (_reactions[legacies[reaction]]) { + _reactions[legacies[reaction]] += reactions[reaction]; + } else { + _reactions[legacies[reaction]] = reactions[reaction]; + } + } else { + if (_reactions[reaction]) { + _reactions[reaction] += reactions[reaction]; + } else { + _reactions[reaction] = reactions[reaction]; + } + } + } + + const _reactions2 = {} as Record; + + for (const reaction of Object.keys(_reactions)) { + _reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; + } + + return _reactions2; + } + + public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise { + if (reaction == null) return await this.getFallbackReaction(); + + reacterHost = this.utilityService.toPunyNullable(reacterHost); + + // 文字列タイプのリアクションを絵文字に変換 + if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; + + // Unicode絵文字 + const match = emojiRegex.exec(reaction); + if (match) { + // 合字を含む1つの絵文字 + const unicode = match[0]; + + // 異体字セレクタ除去 + return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); + } + + const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); + if (custom) { + const name = custom[1]; + const emoji = await this.emojisRepository.findOneBy({ + host: reacterHost ?? IsNull(), + name, + }); + + if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; + } + + return await this.getFallbackReaction(); + } + + public decodeReaction(str: string): DecodedReaction { + const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); + + if (custom) { + const name = custom[1]; + const host = custom[2] ?? null; + + return { + reaction: `:${name}@${host ?? '.'}:`, // ローカル分は@以降を省略するのではなく.にする + name, + host, + }; + } + + return { + reaction: str, + name: undefined, + host: undefined, + }; + } + + public convertLegacyReaction(reaction: string): string { + reaction = this.decodeReaction(reaction).reaction; + if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; + return reaction; + } +} diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts new file mode 100644 index 0000000000..c1d85e8b48 --- /dev/null +++ b/packages/backend/src/core/RelayService.ts @@ -0,0 +1,119 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import type { ILocalUser, User } from '@/models/entities/User.js'; +import { RelaysRepository, UsersRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Cache } from '@/misc/cache.js'; +import type { Relay } from '@/models/entities/Relay.js'; +import { QueueService } from '@/core/QueueService.js'; +import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { DI } from '@/di-symbols.js'; + +const ACTOR_USERNAME = 'relay.actor' as const; + +@Injectable() +export class RelayService { + #relaysCache: Cache; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.relaysRepository) + private relaysRepository: RelaysRepository, + + private idService: IdService, + private queueService: QueueService, + private createSystemUserService: CreateSystemUserService, + private apRendererService: ApRendererService, + ) { + this.#relaysCache = new Cache(1000 * 60 * 10); + } + + async #getRelayActor(): Promise { + const user = await this.usersRepository.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, + }); + + if (user) return user as ILocalUser; + + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); + return created as ILocalUser; + } + + public async addRelay(inbox: string): Promise { + const relay = await this.relaysRepository.insert({ + id: this.idService.genId(), + inbox, + status: 'requesting', + }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); + + const relayActor = await this.#getRelayActor(); + const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); + const activity = this.apRendererService.renderActivity(follow); + this.queueService.deliver(relayActor, activity, relay.inbox); + + return relay; + } + + public async removeRelay(inbox: string): Promise { + const relay = await this.relaysRepository.findOneBy({ + inbox, + }); + + if (relay == null) { + throw new Error('relay not found'); + } + + const relayActor = await this.#getRelayActor(); + const follow = this.apRendererService.renderFollowRelay(relay, relayActor); + const undo = this.apRendererService.renderUndo(follow, relayActor); + const activity = this.apRendererService.renderActivity(undo); + this.queueService.deliver(relayActor, activity, relay.inbox); + + await this.relaysRepository.delete(relay.id); + } + + public async listRelay(): Promise { + const relays = await this.relaysRepository.find(); + return relays; + } + + public async relayAccepted(id: string): Promise { + const result = await this.relaysRepository.update(id, { + status: 'accepted', + }); + + return JSON.stringify(result); + } + + public async relayRejected(id: string): Promise { + const result = await this.relaysRepository.update(id, { + status: 'rejected', + }); + + return JSON.stringify(result); + } + + public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { + if (activity == null) return; + + const relays = await this.#relaysCache.fetch(null, () => this.relaysRepository.findBy({ + status: 'accepted', + })); + if (relays.length === 0) return; + + // TODO + //const copy = structuredClone(activity); + const copy = JSON.parse(JSON.stringify(activity)); + if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; + + const signed = await this.apRendererService.attachLdSignature(copy, user); + + for (const relay of relays) { + this.queueService.deliver(user, signed, relay.inbox); + } + } +} diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts new file mode 100644 index 0000000000..9549e19990 --- /dev/null +++ b/packages/backend/src/core/S3Service.ts @@ -0,0 +1,38 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import S3 from 'aws-sdk/clients/s3.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { Meta } from '@/models/entities/Meta.js'; +import { HttpRequestService } from './HttpRequestService.js'; + +@Injectable() +export class S3Service { + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + } + + public getS3(meta: Meta) { + const u = meta.objectStorageEndpoint != null + ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` + : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; + + return new S3({ + endpoint: meta.objectStorageEndpoint ?? undefined, + accessKeyId: meta.objectStorageAccessKey!, + secretAccessKey: meta.objectStorageSecretKey!, + region: meta.objectStorageRegion ?? undefined, + sslEnabled: meta.objectStorageUseSSL, + s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted + ? false + : meta.objectStorageS3ForcePathStyle, + httpOptions: { + agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), + }, + }); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts new file mode 100644 index 0000000000..a876668b94 --- /dev/null +++ b/packages/backend/src/core/SignupService.ts @@ -0,0 +1,141 @@ +import { generateKeyPair } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import { DataSource, IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UsedUsernamesRepository } from '@/models/index.js'; +import { Config } from '@/config.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 generateUserToken from '@/misc/generate-native-user-token.js'; +import UsersChart from './chart/charts/users.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { UtilityService } from './UtilityService.js'; + +@Injectable() +export class SignupService { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.usedUsernamesRepository) + private usedUsernamesRepository: UsedUsernamesRepository, + + private utilityService: UtilityService, + private userEntityService: UserEntityService, + private idService: IdService, + private usersChart: UsersChart, + ) { + } + + public async signup(opts: { + username: User['username']; + password?: string | null; + passwordHash?: UserProfile['password'] | null; + host?: string | null; + }) { + const { username, password, passwordHash, host } = opts; + let hash = passwordHash; + + // Validate username + if (!this.userEntityService.validateLocalUsername(username)) { + throw new Error('INVALID_USERNAME'); + } + + if (password != null && passwordHash == null) { + // Validate password + if (!this.userEntityService.validatePassword(password)) { + throw new Error('INVALID_PASSWORD'); + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + hash = await bcrypt.hash(password, salt); + } + + // Generate secret + const secret = generateUserToken(); + + // Check username duplication + if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { + throw new Error('DUPLICATED_USERNAME'); + } + + // Check deleted username duplication + if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { + throw new Error('USED_USERNAME'); + } + + const keyPair = await new Promise((res, rej) => + generateKeyPair('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: undefined, + passphrase: undefined, + }, + } as any, (err, publicKey, privateKey) => + err ? rej(err) : res([publicKey, privateKey]), + )); + + 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 username is already used'); + + account = await transactionalEntityManager.save(new User({ + id: this.idService.genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: this.utilityService.toPunyNullable(host), + token: secret, + isAdmin: (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0, + })); + + await transactionalEntityManager.save(new UserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], + userId: account.id, + })); + + await transactionalEntityManager.save(new UserProfile({ + userId: account.id, + autoAcceptFollowed: true, + password: hash, + })); + + await transactionalEntityManager.save(new UsedUsername({ + createdAt: new Date(), + username: username.toLowerCase(), + })); + }); + + this.usersChart.update(account, true); + + return { account, secret }; + } +} + diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts new file mode 100644 index 0000000000..be31534c02 --- /dev/null +++ b/packages/backend/src/core/TwoFactorAuthenticationService.ts @@ -0,0 +1,439 @@ +import * as crypto from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import * as jsrsasign from 'jsrsasign'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; + +const ECC_PRELUDE = Buffer.from([0x04]); +const NULL_BYTE = Buffer.from([0]); +const PEM_PRELUDE = Buffer.from( + '3059301306072a8648ce3d020106082a8648ce3d030107034200', + 'hex', +); + +// Android Safetynet attestations are signed with this cert: +const GSR2 = `-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE-----\n`; + +function base64URLDecode(source: string) { + return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64'); +} + +function getCertSubject(certificate: string) { + const subjectCert = new jsrsasign.X509(); + subjectCert.readCertPEM(certificate); + + const subjectString = subjectCert.getSubjectString(); + const subjectFields = subjectString.slice(1).split('/'); + + const fields = {} as Record; + for (const field of subjectFields) { + const eqIndex = field.indexOf('='); + fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1); + } + + return fields; +} + +function verifyCertificateChain(certificates: string[]) { + let valid = true; + + for (let i = 0; i < certificates.length; i++) { + const Cert = certificates[i]; + const certificate = new jsrsasign.X509(); + certificate.readCertPEM(Cert); + + const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; + + const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); + const algorithm = certificate.getSignatureAlgorithmField(); + const signatureHex = certificate.getSignatureValueHex(); + + // Verify against CA + const Signature = new jsrsasign.KJUR.crypto.Signature({ alg: algorithm }); + Signature.init(CACert); + Signature.updateHex(certStruct); + valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate + } + + return valid; +} + +function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { + if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) { + pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91); + type = 'PUBLIC KEY'; + } + const cert = pemBuffer.toString('base64'); + + const keyParts = []; + const max = Math.ceil(cert.length / 64); + let start = 0; + for (let i = 0; i < max; i++) { + keyParts.push(cert.substring(start, start + 64)); + start += 64; + } + + return ( + `-----BEGIN ${type}-----\n` + + keyParts.join('\n') + + `\n-----END ${type}-----\n` + ); +} + +@Injectable() +export class TwoFactorAuthenticationService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + } + + public hash(data: Buffer) { + return crypto + .createHash('sha256') + .update(data) + .digest(); + } + + public verifySignin({ + publicKey, + authenticatorData, + clientDataJSON, + clientData, + signature, + challenge, + }: { + publicKey: Buffer, + authenticatorData: Buffer, + clientDataJSON: Buffer, + clientData: any, + signature: Buffer, + challenge: string + }) { + if (clientData.type !== 'webauthn.get') { + throw new Error('type is not webauthn.get'); + } + + if (this.hash(clientData.challenge).toString('hex') !== challenge) { + throw new Error('challenge mismatch'); + } + if (clientData.origin !== this.config.scheme + '://' + this.config.host) { + throw new Error('origin mismatch'); + } + + const verificationData = Buffer.concat( + [authenticatorData, this.hash(clientDataJSON)], + 32 + authenticatorData.length, + ); + + return crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(publicKey), signature); + } + + public getProcedures() { + return { + none: { + verify({ publicKey }: { publicKey: Map }) { + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyU2F = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + return { + publicKey: publicKeyU2F, + valid: true, + }; + }, + }, + 'android-key': { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) { + if (attStmt.alg !== -7) { + throw new Error('alg mismatch'); + } + + const verificationData = Buffer.concat([ + authenticatorData, + clientDataHash, + ]); + + const attCert: Buffer = attStmt.x5c[0]; + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + if (!attCert.equals(publicKeyData)) { + throw new Error('public key mismatch'); + } + + const isValid = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) + + return { + valid: isValid, + publicKey: publicKeyData, + }; + }, + }, + // what a stupid attestation + 'android-safetynet': { + verify: ({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) => { + const verificationData = this.hash( + Buffer.concat([authenticatorData, clientDataHash]), + ); + + const jwsParts = attStmt.response.toString('utf-8').split('.'); + + const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); + const response = JSON.parse( + base64URLDecode(jwsParts[1]).toString('utf-8'), + ); + const signature = jwsParts[2]; + + if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { + throw new Error('invalid nonce'); + } + + const certificateChain = header.x5c + .map((key: any) => PEMString(key)) + .concat([GSR2]); + + if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { + throw new Error('invalid common name'); + } + + if (!verifyCertificateChain(certificateChain)) { + throw new Error('Invalid certificate chain!'); + } + + const signatureBase = Buffer.from( + jwsParts[0] + '.' + jwsParts[1], + 'utf-8', + ); + + const valid = crypto + .createVerify('sha256') + .update(signatureBase) + .verify(certificateChain[0], base64URLDecode(signature)); + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + return { + valid, + publicKey: publicKeyData, + }; + }, + }, + packed: { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) { + const verificationData = Buffer.concat([ + authenticatorData, + clientDataHash, + ]); + + if (attStmt.x5c) { + const attCert = attStmt.x5c[0]; + + const validSignature = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + return { + valid: validSignature, + publicKey: publicKeyData, + }; + } else if (attStmt.ecdaaKeyId) { + // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation + throw new Error('ECDAA-Verify is not supported'); + } else { + if (attStmt.alg !== -7) throw new Error('alg mismatch'); + + throw new Error('self attestation is not supported'); + } + }, + }, + + 'fido-u2f': { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map, + rpIdHash: Buffer, + credentialId: Buffer + }) { + const x5c: Buffer[] = attStmt.x5c; + if (x5c.length !== 1) { + throw new Error('x5c length does not match expectation'); + } + + const attCert = x5c[0]; + + // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve + + const negTwo: Buffer = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree: Buffer = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyU2F = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + const verificationData = Buffer.concat([ + NULL_BYTE, + rpIdHash, + clientDataHash, + credentialId, + publicKeyU2F, + ]); + + const validSignature = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + return { + valid: validSignature, + publicKey: publicKeyU2F, + }; + }, + }, + }; + } +} diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts new file mode 100644 index 0000000000..a9396fcbba --- /dev/null +++ b/packages/backend/src/core/UserBlockingService.ts @@ -0,0 +1,199 @@ + +import { Inject, Injectable } from '@nestjs/common'; +import { IdService } from '@/core/IdService.js'; +import type { CacheableUser, User } from '@/models/entities/User.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; +import { QueueService } from '@/core/QueueService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { WebhookService } from './WebhookService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; + +@Injectable() +export class UserBlockingService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private queueService: QueueService, + private globalEventServie: GlobalEventService, + private webhookService: WebhookService, + private apRendererService: ApRendererService, + private perUserFollowingChart: PerUserFollowingChart, + ) { + } + + public async block(blocker: User, blockee: User) { + await Promise.all([ + this.#cancelRequest(blocker, blockee), + this.#cancelRequest(blockee, blocker), + this.#unFollow(blocker, blockee), + this.#unFollow(blockee, blocker), + this.#removeFromList(blockee, blocker), + ]); + + const blocking = { + id: this.idService.genId(), + createdAt: new Date(), + blocker, + blockerId: blocker.id, + blockee, + blockeeId: blockee.id, + } as Blocking; + + await this.blockingsRepository.insert(blocking); + + if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); + this.queueService.deliver(blocker, content, blockee.inbox); + } + } + + async #cancelRequest(follower: User, followee: User) { + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (request == null) { + return; + } + + await this.followRequestsRepository.delete({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (this.userEntityService.isLocalUser(followee)) { + this.userEntityService.pack(followee, followee, { + detail: true, + }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + } + + if (this.userEntityService.isLocalUser(follower)) { + this.userEntityService.pack(followee, follower, { + detail: true, + }).then(async packed => { + this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } + }); + } + + // リモートにフォローリクエストをしていたらUndoFollow送信 + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + this.queueService.deliver(follower, content, followee.inbox); + } + + // リモートからフォローリクエストを受けていたらReject送信 + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + this.queueService.deliver(followee, content, follower.inbox); + } + } + + async #unFollow(follower: User, followee: User) { + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (following == null) { + return; + } + + await Promise.all([ + this.followingsRepository.delete(following.id), + this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), + this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), + this.perUserFollowingChart.update(follower, followee, false), + ]); + + // Publish unfollow event + if (this.userEntityService.isLocalUser(follower)) { + this.userEntityService.pack(followee, follower, { + detail: true, + }).then(async packed => { + this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } + }); + } + + // リモートにフォローをしていたらUndoFollow送信 + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + this.queueService.deliver(follower, content, followee.inbox); + } + } + + async #removeFromList(listOwner: User, user: User) { + const userLists = await this.userListsRepository.findBy({ + userId: listOwner.id, + }); + + for (const userList of userLists) { + await this.userListJoiningsRepository.delete({ + userListId: userList.id, + userId: user.id, + }); + } + } + + public async unblock(blocker: CacheableUser, blockee: CacheableUser) { + const blocking = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + }); + + if (blocking == null) { + logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした'); + return; + } + + // Since we already have the blocker and blockee, we do not need to fetch + // them in the query above and can just manually insert them here. + blocking.blocker = blocker; + blocking.blockee = blockee; + + await this.blockingsRepository.delete(blocking.id); + + // deliver if remote bloking + if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); + this.queueService.deliver(blocker, content, blockee.inbox); + } + } +} diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts new file mode 100644 index 0000000000..8212abf7bb --- /dev/null +++ b/packages/backend/src/core/UserCacheService.ts @@ -0,0 +1,74 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import { UsersRepository } from '@/models/index.js'; +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 './entities/UserEntityService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class UserCacheService implements OnApplicationShutdown { + public userByIdCache: Cache; + public localUserByNativeTokenCache: Cache; + public localUserByIdCache: Cache; + public uriPersonCache: Cache; + + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + ) { + this.onMessage = this.onMessage.bind(this); + + this.userByIdCache = new Cache(Infinity); + this.localUserByNativeTokenCache = new Cache(Infinity); + this.localUserByIdCache = new Cache(Infinity); + this.uriPersonCache = new Cache(Infinity); + + this.redisSubscriber.on('message', this.onMessage); + } + + private async onMessage(_, data) { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + 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); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value?.id === user.id) { + this.uriPersonCache.set(k, user); + } + } + if (this.userEntityService.isLocalUser(user)) { + this.localUserByNativeTokenCache.set(user.token, user); + this.localUserByIdCache.set(user.id, user); + } + break; + } + case 'userTokenRegenerated': { + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as ILocalUser; + this.localUserByNativeTokenCache.delete(body.oldToken); + this.localUserByNativeTokenCache.set(body.newToken, user); + break; + } + default: + break; + } + } + } + + 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 new file mode 100644 index 0000000000..6aae5a6b99 --- /dev/null +++ b/packages/backend/src/core/UserFollowingService.ts @@ -0,0 +1,574 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { CacheableUser, ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { QueueService } from '@/core/QueueService.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import type { Packed } from '@/misc/schema.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { WebhookService } from '@/core/WebhookService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { DI } from '@/di-symbols.js'; +import Logger from '../logger.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; + +const logger = new Logger('following/create'); + +type Local = ILocalUser | { + id: ILocalUser['id']; + host: ILocalUser['host']; + uri: ILocalUser['uri'] +}; +type Remote = IRemoteUser | { + id: IRemoteUser['id']; + host: IRemoteUser['host']; + uri: IRemoteUser['uri']; + inbox: IRemoteUser['inbox']; +}; +type Both = Local | Remote; + +@Injectable() +export class UserFollowingService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private queueService: QueueService, + private globalEventServie: GlobalEventService, + private createNotificationService: CreateNotificationService, + private federatedInstanceService: FederatedInstanceService, + private webhookService: WebhookService, + private apRendererService: ApRendererService, + private perUserFollowingChart: PerUserFollowingChart, + private instanceChart: InstanceChart, + ) { + } + + 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 }), + this.usersRepository.findOneByOrFail({ id: _followee.id }), + ]); + + // check blocking + const [blocking, blocked] = await Promise.all([ + this.blockingsRepository.findOneBy({ + blockerId: follower.id, + blockeeId: followee.id, + }), + this.blockingsRepository.findOneBy({ + blockerId: followee.id, + blockeeId: follower.id, + }), + ]); + + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { + // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 + const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox); + return; + } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { + // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 + await this.blockingsRepository.delete(blocking.id); + } else { + // それ以外は単純に例外 + if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); + if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); + } + + const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); + + // フォロー対象が鍵アカウントである or + // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or + // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである + // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく + if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) { + let autoAccept = false; + + // 鍵アカウントであっても、既にフォローされていた場合はスルー + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + if (following) { + autoAccept = true; + } + + // フォローしているユーザーは自動承認オプション + if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { + const followed = await this.followingsRepository.findOneBy({ + followerId: followee.id, + followeeId: follower.id, + }); + + if (followed) autoAccept = true; + } + + if (!autoAccept) { + await this.createFollowRequest(follower, followee, requestId); + return; + } + } + + await this.#insertFollowingDoc(followee, follower); + + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox); + } + } + + async #insertFollowingDoc( + followee: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] + }, + follower: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] + }, + ): Promise { + if (follower.id === followee.id) return; + + let alreadyFollowed = false as boolean; + + await this.followingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + + // 非正規化 + followerHost: follower.host, + followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null, + followerSharedInbox: this.userEntityService.isRemoteUser(follower) ? follower.sharedInbox : null, + followeeHost: followee.host, + followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : null, + followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null, + }).catch(err => { + if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); + alreadyFollowed = true; + } else { + throw err; + } + }); + + const req = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (req) { + await this.followRequestsRepository.delete({ + followeeId: followee.id, + followerId: follower.id, + }); + + // 通知を作成 + this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', { + notifierId: followee.id, + }); + } + + if (alreadyFollowed) return; + + //#region Increment counts + await Promise.all([ + this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), + this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), + ]); + //#endregion + + //#region Update instance stats + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => { + this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); + this.instanceChart.updateFollowing(i.host, true); + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => { + this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); + this.instanceChart.updateFollowers(i.host, true); + }); + } + //#endregion + + this.perUserFollowingChart.update(follower, followee, true); + + // Publish follow event + if (this.userEntityService.isLocalUser(follower)) { + this.userEntityService.pack(followee.id, follower, { + detail: true, + }).then(async packed => { + this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'follow', { + user: packed, + }); + } + }); + } + + // Publish followed event + if (this.userEntityService.isLocalUser(followee)) { + this.userEntityService.pack(follower.id, followee).then(async packed => { + this.globalEventServie.publishMainStream(followee.id, 'followed', packed); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'followed', { + user: packed, + }); + } + }); + + // 通知を作成 + this.createNotificationService.createNotification(followee.id, 'follow', { + notifierId: follower.id, + }); + } + } + + public async unfollow( + follower: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + followee: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + silent = false, + ): Promise { + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (following == null) { + logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + return; + } + + await this.followingsRepository.delete(following.id); + + this.#decrementFollowing(follower, followee); + + // Publish unfollow event + if (!silent && this.userEntityService.isLocalUser(follower)) { + this.userEntityService.pack(followee.id, follower, { + detail: true, + }).then(async packed => { + this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } + }); + } + + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + this.queueService.deliver(follower, content, followee.inbox); + } + + if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { + // local user has null host + const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); + this.queueService.deliver(followee, content, follower.inbox); + } + } + + async #decrementFollowing( + follower: {id: User['id']; host: User['host']; }, + followee: { id: User['id']; host: User['host']; }, + ): Promise { + //#region Decrement following / followers counts + await Promise.all([ + this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), + this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), + ]); + //#endregion + + //#region Update instance stats + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => { + this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); + this.instanceChart.updateFollowing(i.host, false); + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => { + this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); + this.instanceChart.updateFollowers(i.host, false); + }); + } + //#endregion + + this.perUserFollowingChart.update(follower, followee, false); + } + + public async createFollowRequest( + follower: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + followee: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + requestId?: string, + ): Promise { + if (follower.id === followee.id) return; + + // check blocking + const [blocking, blocked] = await Promise.all([ + this.blockingsRepository.findOneBy({ + blockerId: follower.id, + blockeeId: followee.id, + }), + this.blockingsRepository.findOneBy({ + blockerId: followee.id, + blockeeId: follower.id, + }), + ]); + + if (blocking != null) throw new Error('blocking'); + if (blocked != null) throw new Error('blocked'); + + const followRequest = await this.followRequestsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + requestId, + + // 非正規化 + followerHost: follower.host, + followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined, + followerSharedInbox: this.userEntityService.isRemoteUser(follower) ? follower.sharedInbox : undefined, + followeeHost: followee.host, + followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, + followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, + }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish receiveRequest event + if (this.userEntityService.isLocalUser(followee)) { + this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed)); + + this.userEntityService.pack(followee.id, followee, { + detail: true, + }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + + // 通知を作成 + this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { + notifierId: follower.id, + followRequestId: followRequest.id, + }); + } + + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)); + this.queueService.deliver(follower, content, followee.inbox); + } + } + + public async cancelFollowRequest( + followee: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] + }, + follower: { + id: User['id']; host: User['host']; uri: User['host'] + }, + ): Promise { + if (this.userEntityService.isRemoteUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + + if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので + this.queueService.deliver(follower, content, followee.inbox); + } + } + + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (request == null) { + throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); + } + + await this.followRequestsRepository.delete({ + followeeId: followee.id, + followerId: follower.id, + }); + + this.userEntityService.pack(followee.id, followee, { + detail: true, + }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + } + + public async acceptFollowRequest( + followee: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + follower: CacheableUser, + ): Promise { + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (request == null) { + throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); + } + + await this.#insertFollowingDoc(followee, follower); + + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + this.queueService.deliver(followee, content, follower.inbox); + } + + this.userEntityService.pack(followee.id, followee, { + detail: true, + }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + } + + public async acceptAllFollowRequests( + user: { + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + }, + ): Promise { + const requests = await this.followRequestsRepository.findBy({ + followeeId: user.id, + }); + + for (const request of requests) { + const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId }); + this.acceptFollowRequest(user, follower); + } + } + + /** + * API following/request/reject + */ + public async rejectFollowRequest(user: Local, follower: Both): Promise { + if (this.userEntityService.isRemoteUser(follower)) { + this.#deliverReject(user, follower); + } + + await this.#removeFollowRequest(user, follower); + + if (this.userEntityService.isLocalUser(follower)) { + this.#publishUnfollow(user, follower); + } + } + + /** + * API following/reject + */ + public async rejectFollow(user: Local, follower: Both): Promise { + if (this.userEntityService.isRemoteUser(follower)) { + this.#deliverReject(user, follower); + } + + await this.#removeFollow(user, follower); + + if (this.userEntityService.isLocalUser(follower)) { + this.#publishUnfollow(user, follower); + } + } + + /** + * AP Reject/Follow + */ + public async remoteReject(actor: Remote, follower: Local): Promise { + await this.#removeFollowRequest(actor, follower); + await this.#removeFollow(actor, follower); + this.#publishUnfollow(actor, follower); + } + + /** + * Remove follow request record + */ + async #removeFollowRequest(followee: Both, follower: Both): Promise { + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (!request) return; + + await this.followRequestsRepository.delete(request.id); + } + + /** + * Remove follow record + */ + async #removeFollow(followee: Both, follower: Both): Promise { + const following = await this.followingsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + if (!following) return; + + await this.followingsRepository.delete(following.id); + this.#decrementFollowing(follower, followee); + } + + /** + * Deliver Reject to remote + */ + async #deliverReject(followee: Local, follower: Remote): Promise { + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, + }); + + const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); + this.queueService.deliver(followee, content, follower.inbox); + } + + /** + * Publish unfollow to local + */ + async #publishUnfollow(followee: Both, follower: Local): Promise { + const packedFollowee = await this.userEntityService.pack(followee.id, follower, { + detail: true, + }); + + this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee); + this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packedFollowee, + }); + } + } +} diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts new file mode 100644 index 0000000000..f093d2ea91 --- /dev/null +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { User } from '@/models/entities/User.js'; +import { 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'; + +@Injectable() +export class UserKeypairStoreService { + #cache: Cache; + + constructor( + @Inject(DI.userKeypairsRepository) + private userKeypairsRepository: UserKeypairsRepository, + ) { + this.#cache = new Cache(Infinity); + } + + 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 new file mode 100644 index 0000000000..03113f042a --- /dev/null +++ b/packages/backend/src/core/UserListService.ts @@ -0,0 +1,48 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { UserListJoining } from '@/models/entities/UserListJoining.js'; +import { IdService } from '@/core/IdService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { ProxyAccountService } from './ProxyAccountService.js'; + +@Injectable() +export class UserListService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private userFollowingService: UserFollowingService, + private globalEventServie: GlobalEventService, + private proxyAccountService: ProxyAccountService, + ) { + } + + public async push(target: User, list: UserList) { + await this.userListJoiningsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: target.id, + userListId: list.id, + } as UserListJoining); + + this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); + + // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする + if (this.userEntityService.isRemoteUser(target)) { + const proxy = await this.proxyAccountService.fetch(); + if (proxy) { + this.userFollowingService.follow(proxy, target); + } + } + } +} diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts new file mode 100644 index 0000000000..9146360df1 --- /dev/null +++ b/packages/backend/src/core/UserMutingService.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, MutingsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +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'; + +@Injectable() +export class UserMutingService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private idService: IdService, + private queueService: QueueService, + private globalEventServie: GlobalEventService, + ) { + } + + public async mute(user: User, target: User): Promise { + await this.mutingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + muterId: user.id, + muteeId: target.id, + }); + } +} diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts new file mode 100644 index 0000000000..068341cb2c --- /dev/null +++ b/packages/backend/src/core/UserSuspendService.ts @@ -0,0 +1,88 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Not, IsNull } from 'typeorm'; +import { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { QueueService } from '@/core/QueueService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { ApRendererService } from './remote/activitypub/ApRendererService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; + +@Injectable() +export class UserSuspendService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private queueService: QueueService, + private globalEventService: GlobalEventService, + private apRendererService: ApRendererService, + ) { + } + + public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { + this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); + + if (this.userEntityService.isLocalUser(user)) { + // 知り得る全SharedInboxにDelete配信 + const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user)); + + const queue: string[] = []; + + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); + + for (const inbox of inboxes) { + if (inbox != null && !queue.includes(inbox)) queue.push(inbox); + } + + for (const inbox of queue) { + this.queueService.deliver(user, content, inbox); + } + } + } + + public async doPostUnsuspend(user: User): Promise { + this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); + + if (this.userEntityService.isLocalUser(user)) { + // 知り得る全SharedInboxにUndo Delete配信 + const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user)); + + const queue: string[] = []; + + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); + + for (const inbox of inboxes) { + if (inbox != null && !queue.includes(inbox)) queue.push(inbox); + } + + for (const inbox of queue) { + this.queueService.deliver(user as any, content, inbox); + } + } + } +} diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts new file mode 100644 index 0000000000..ba03dfc069 --- /dev/null +++ b/packages/backend/src/core/UtilityService.ts @@ -0,0 +1,37 @@ +import { URL } from 'node:url'; +import { toASCII } from 'punycode'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; + +@Injectable() +export class UtilityService { + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + public getFullApAccount(username: string, host: string | null): string { + return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`; + } + + public isSelfHost(host: string | null): boolean { + if (host == null) return true; + return this.toPuny(this.config.host) === this.toPuny(host); + } + + public extractDbHost(uri: string): string { + const url = new URL(uri); + return this.toPuny(url.hostname); + } + + public toPuny(host: string): string { + return toASCII(host.toLowerCase()); + } + + 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 new file mode 100644 index 0000000000..70b9664c76 --- /dev/null +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -0,0 +1,44 @@ +import { Inject, Injectable } from '@nestjs/common'; +import FFmpeg from 'fluent-ffmpeg'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import type { IImage } from '@/core/ImageProcessingService.js'; +import { createTempDir } from '@/misc/create-temp.js'; + +@Injectable() +export class VideoProcessingService { + constructor( + @Inject(DI.config) + private config: Config, + + private imageProcessingService: ImageProcessingService, + ) { + } + + public async generateVideoThumbnail(source: string): Promise { + const [dir, cleanup] = await createTempDir(); + + try { + await new Promise((res, rej) => { + FFmpeg({ + source, + }) + .on('end', res) + .on('error', rej) + .screenshot({ + folder: dir, + filename: 'out.png', // must have .png extension + count: 1, + timestamps: ['5%'], + }); + }); + + // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) + return await this.imageProcessingService.convertToJpeg(`${dir}/out.png`, 498, 280); + } finally { + cleanup(); + } + } +} + diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts new file mode 100644 index 0000000000..3ed615ca0b --- /dev/null +++ b/packages/backend/src/core/WebhookService.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import { 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'; + +@Injectable() +export class WebhookService implements OnApplicationShutdown { + #webhooksFetched = false; + #webhooks: Webhook[] = []; + + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + this.onMessage = this.onMessage.bind(this); + this.redisSubscriber.on('message', this.onMessage); + } + + public async getActiveWebhooks() { + if (!this.#webhooksFetched) { + this.#webhooks = await this.webhooksRepository.findBy({ + active: true, + }); + this.#webhooksFetched = true; + } + + return this.#webhooks; + } + + private async onMessage(_, data) { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message; + switch (type) { + case 'webhookCreated': + if (body.active) { + this.#webhooks.push(body); + } + break; + case 'webhookUpdated': + if (body.active) { + const i = this.#webhooks.findIndex(a => a.id === body.id); + if (i > -1) { + this.#webhooks[i] = body; + } else { + this.#webhooks.push(body); + } + } else { + this.#webhooks = this.#webhooks.filter(a => a.id !== body.id); + } + break; + case 'webhookDeleted': + this.#webhooks = this.#webhooks.filter(a => a.id !== body.id); + break; + default: + break; + } + } + } + + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } +} diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts new file mode 100644 index 0000000000..2020ca0ca6 --- /dev/null +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -0,0 +1,59 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { beforeShutdown } from '@/misc/before-shutdown.js'; + +import FederationChart from './charts/federation.js'; +import NotesChart from './charts/notes.js'; +import UsersChart from './charts/users.js'; +import ActiveUsersChart from './charts/active-users.js'; +import InstanceChart from './charts/instance.js'; +import PerUserNotesChart from './charts/per-user-notes.js'; +import DriveChart from './charts/drive.js'; +import PerUserReactionsChart from './charts/per-user-reactions.js'; +import HashtagChart from './charts/hashtag.js'; +import PerUserFollowingChart from './charts/per-user-following.js'; +import PerUserDriveChart from './charts/per-user-drive.js'; +import ApRequestChart from './charts/ap-request.js'; + +@Injectable() +export class ChartManagementService { + constructor( + private federationChart: FederationChart, + private notesChart: NotesChart, + private usersChart: UsersChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + private perUserNotesChart: PerUserNotesChart, + private driveChart: DriveChart, + private perUserReactionsChart: PerUserReactionsChart, + private hashtagChart: HashtagChart, + private perUserFollowingChart: PerUserFollowingChart, + private perUserDriveChart: PerUserDriveChart, + private apRequestChart: ApRequestChart, + ) {} + + public async run() { + const charts = [ + this.federationChart, + this.notesChart, + this.usersChart, + this.activeUsersChart, + this.instanceChart, + this.perUserNotesChart, + this.driveChart, + this.perUserReactionsChart, + this.hashtagChart, + this.perUserFollowingChart, + this.perUserDriveChart, + this.apRequestChart, + ]; + + // 20分おきにメモリ情報をDBに書き込み + setInterval(() => { + for (const chart of charts) { + chart.save(); + } + }, 1000 * 60 * 20); + + beforeShutdown(() => Promise.all(charts.map(chart => chart.save()))); + } +} diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts new file mode 100644 index 0000000000..a5d9f166ed --- /dev/null +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -0,0 +1,54 @@ +import { Injectable, Inject } from '@nestjs/common'; +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 Chart from '../core.js'; +import { name, schema } from './entities/active-users.js'; +import type { KVs } from '../core.js'; + +const week = 1000 * 60 * 60 * 24 * 7; +const month = 1000 * 60 * 60 * 24 * 30; +const year = 1000 * 60 * 60 * 24 * 365; + +/** + * アクティブユーザーに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class ActiveUsersChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { + await this.commit({ + 'read': [user.id], + 'registeredWithinWeek': (Date.now() - user.createdAt.getTime() < week) ? [user.id] : [], + 'registeredWithinMonth': (Date.now() - user.createdAt.getTime() < month) ? [user.id] : [], + 'registeredWithinYear': (Date.now() - user.createdAt.getTime() < year) ? [user.id] : [], + 'registeredOutsideWeek': (Date.now() - user.createdAt.getTime() > week) ? [user.id] : [], + 'registeredOutsideMonth': (Date.now() - user.createdAt.getTime() > month) ? [user.id] : [], + 'registeredOutsideYear': (Date.now() - user.createdAt.getTime() > year) ? [user.id] : [], + }); + } + + 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 new file mode 100644 index 0000000000..c857cea98c --- /dev/null +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -0,0 +1,49 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/ap-request.js'; +import type { KVs } from '../core.js'; + +/** + * Chart about ActivityPub requests + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class ApRequestChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async deliverSucc(): Promise { + await this.commit({ + 'deliverSucceeded': 1, + }); + } + + public async deliverFail(): Promise { + await this.commit({ + 'deliverFailed': 1, + }); + } + + 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 new file mode 100644 index 0000000000..dd6d002030 --- /dev/null +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -0,0 +1,47 @@ +import { Injectable, Inject } from '@nestjs/common'; +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 Chart from '../core.js'; +import { name, schema } from './entities/drive.js'; +import type { KVs } from '../core.js'; + +/** + * ドライブに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class DriveChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(file: DriveFile, isAdditional: boolean): Promise { + const fileSizeKb = file.size / 1000; + await this.commit(file.userHost === null ? { + 'local.incCount': isAdditional ? 1 : 0, + 'local.incSize': isAdditional ? fileSizeKb : 0, + 'local.decCount': isAdditional ? 0 : 1, + 'local.decSize': isAdditional ? 0 : fileSizeKb, + } : { + 'remote.incCount': isAdditional ? 1 : 0, + 'remote.incSize': isAdditional ? fileSizeKb : 0, + 'remote.decCount': isAdditional ? 0 : 1, + 'remote.decSize': isAdditional ? 0 : fileSizeKb, + }); + } +} diff --git a/packages/backend/src/core/chart/charts/entities/active-users.ts b/packages/backend/src/core/chart/charts/entities/active-users.ts new file mode 100644 index 0000000000..5767b76f8e --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/active-users.ts @@ -0,0 +1,17 @@ +import Chart from '../../core.js'; + +export const name = 'activeUsers'; + +export const schema = { + 'readWrite': { intersection: ['read', 'write'], range: 'small' }, + 'read': { uniqueIncrement: true, range: 'small' }, + 'write': { uniqueIncrement: true, range: 'small' }, + 'registeredWithinWeek': { uniqueIncrement: true, range: 'small' }, + 'registeredWithinMonth': { uniqueIncrement: true, range: 'small' }, + 'registeredWithinYear': { uniqueIncrement: true, range: 'small' }, + 'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' }, + 'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' }, + 'registeredOutsideYear': { uniqueIncrement: true, range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/ap-request.ts b/packages/backend/src/core/chart/charts/entities/ap-request.ts new file mode 100644 index 0000000000..3a9f3dacfd --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/ap-request.ts @@ -0,0 +1,11 @@ +import Chart from '../../core.js'; + +export const name = 'apRequest'; + +export const schema = { + 'deliverFailed': { }, + 'deliverSucceeded': { }, + 'inboxReceived': { }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/drive.ts b/packages/backend/src/core/chart/charts/entities/drive.ts new file mode 100644 index 0000000000..4bf5bb729e --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/drive.ts @@ -0,0 +1,16 @@ +import Chart from '../../core.js'; + +export const name = 'drive'; + +export const schema = { + 'local.incCount': {}, + 'local.incSize': {}, // in kilobyte + 'local.decCount': {}, + 'local.decSize': {}, // in kilobyte + 'remote.incCount': {}, + 'remote.incSize': {}, // in kilobyte + 'remote.decCount': {}, + 'remote.decSize': {}, // in kilobyte +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/federation.ts b/packages/backend/src/core/chart/charts/entities/federation.ts new file mode 100644 index 0000000000..a8466b0b4c --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/federation.ts @@ -0,0 +1,16 @@ +import Chart from '../../core.js'; + +export const name = 'federation'; + +export const schema = { + 'deliveredInstances': { uniqueIncrement: true, range: 'small' }, + 'inboxInstances': { uniqueIncrement: true, range: 'small' }, + 'stalled': { uniqueIncrement: true, range: 'small' }, + 'sub': { accumulate: true, range: 'small' }, + 'pub': { accumulate: true, range: 'small' }, + 'pubsub': { accumulate: true, range: 'small' }, + 'subActive': { accumulate: true, range: 'small' }, + 'pubActive': { accumulate: true, range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/hashtag.ts b/packages/backend/src/core/chart/charts/entities/hashtag.ts new file mode 100644 index 0000000000..4d04039047 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/hashtag.ts @@ -0,0 +1,10 @@ +import Chart from '../../core.js'; + +export const name = 'hashtag'; + +export const schema = { + 'local.users': { uniqueIncrement: true }, + 'remote.users': { uniqueIncrement: true }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/instance.ts b/packages/backend/src/core/chart/charts/entities/instance.ts new file mode 100644 index 0000000000..06962120e2 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/instance.ts @@ -0,0 +1,32 @@ +import Chart from '../../core.js'; + +export const name = 'instance'; + +export const schema = { + 'requests.failed': { range: 'small' }, + 'requests.succeeded': { range: 'small' }, + 'requests.received': { range: 'small' }, + 'notes.total': { accumulate: true }, + 'notes.inc': {}, + 'notes.dec': {}, + 'notes.diffs.normal': {}, + 'notes.diffs.reply': {}, + 'notes.diffs.renote': {}, + 'notes.diffs.withFile': {}, + 'users.total': { accumulate: true }, + 'users.inc': { range: 'small' }, + 'users.dec': { range: 'small' }, + 'following.total': { accumulate: true }, + 'following.inc': { range: 'small' }, + 'following.dec': { range: 'small' }, + 'followers.total': { accumulate: true }, + 'followers.inc': { range: 'small' }, + 'followers.dec': { range: 'small' }, + 'drive.totalFiles': { accumulate: true }, + 'drive.incFiles': {}, + 'drive.decFiles': {}, + 'drive.incUsage': {}, // in kilobyte + 'drive.decUsage': {}, // in kilobyte +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/notes.ts b/packages/backend/src/core/chart/charts/entities/notes.ts new file mode 100644 index 0000000000..9387dbfb2c --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/notes.ts @@ -0,0 +1,22 @@ +import Chart from '../../core.js'; + +export const name = 'notes'; + +export const schema = { + 'local.total': { accumulate: true }, + 'local.inc': {}, + 'local.dec': {}, + 'local.diffs.normal': {}, + 'local.diffs.reply': {}, + 'local.diffs.renote': {}, + 'local.diffs.withFile': {}, + 'remote.total': { accumulate: true }, + 'remote.inc': {}, + 'remote.dec': {}, + 'remote.diffs.normal': {}, + 'remote.diffs.reply': {}, + 'remote.diffs.renote': {}, + 'remote.diffs.withFile': {}, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts new file mode 100644 index 0000000000..6111640ea0 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts @@ -0,0 +1,14 @@ +import Chart from '../../core.js'; + +export const name = 'perUserDrive'; + +export const schema = { + 'totalCount': { accumulate: true }, + 'totalSize': { accumulate: true }, // in kilobyte + 'incCount': { range: 'small' }, + 'incSize': {}, // in kilobyte + 'decCount': { range: 'small' }, + 'decSize': {}, // in kilobyte +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/per-user-following.ts b/packages/backend/src/core/chart/charts/entities/per-user-following.ts new file mode 100644 index 0000000000..4118daa474 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/per-user-following.ts @@ -0,0 +1,20 @@ +import Chart from '../../core.js'; + +export const name = 'perUserFollowing'; + +export const schema = { + 'local.followings.total': { accumulate: true }, + 'local.followings.inc': { range: 'small' }, + 'local.followings.dec': { range: 'small' }, + 'local.followers.total': { accumulate: true }, + 'local.followers.inc': { range: 'small' }, + 'local.followers.dec': { range: 'small' }, + 'remote.followings.total': { accumulate: true }, + 'remote.followings.inc': { range: 'small' }, + 'remote.followings.dec': { range: 'small' }, + 'remote.followers.total': { accumulate: true }, + 'remote.followers.inc': { range: 'small' }, + 'remote.followers.dec': { range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts new file mode 100644 index 0000000000..c1fa174452 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts @@ -0,0 +1,15 @@ +import Chart from '../../core.js'; + +export const name = 'perUserNotes'; + +export const schema = { + 'total': { accumulate: true }, + 'inc': { range: 'small' }, + 'dec': { range: 'small' }, + 'diffs.normal': { range: 'small' }, + 'diffs.reply': { range: 'small' }, + 'diffs.renote': { range: 'small' }, + 'diffs.withFile': { range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts new file mode 100644 index 0000000000..5e1a6c7b30 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts @@ -0,0 +1,10 @@ +import Chart from '../../core.js'; + +export const name = 'perUserReaction'; + +export const schema = { + 'local.count': { range: 'small' }, + 'remote.count': { range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/test-grouped.ts b/packages/backend/src/core/chart/charts/entities/test-grouped.ts new file mode 100644 index 0000000000..66b6e8e864 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/test-grouped.ts @@ -0,0 +1,11 @@ +import Chart from '../../core.js'; + +export const name = 'testGrouped'; + +export const schema = { + 'foo.total': { accumulate: true }, + 'foo.inc': {}, + 'foo.dec': {}, +} as const; + +export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/entities/test-intersection.ts b/packages/backend/src/core/chart/charts/entities/test-intersection.ts new file mode 100644 index 0000000000..a3bdcb367f --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/test-intersection.ts @@ -0,0 +1,11 @@ +import Chart from '../../core.js'; + +export const name = 'testIntersection'; + +export const schema = { + 'a': { uniqueIncrement: true }, + 'b': { uniqueIncrement: true }, + 'aAndB': { intersection: ['a', 'b'] }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/test-unique.ts b/packages/backend/src/core/chart/charts/entities/test-unique.ts new file mode 100644 index 0000000000..b2cfb71b05 --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/test-unique.ts @@ -0,0 +1,9 @@ +import Chart from '../../core.js'; + +export const name = 'testUnique'; + +export const schema = { + 'foo': { uniqueIncrement: true }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/test.ts b/packages/backend/src/core/chart/charts/entities/test.ts new file mode 100644 index 0000000000..7cba21e16a --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/test.ts @@ -0,0 +1,11 @@ +import Chart from '../../core.js'; + +export const name = 'test'; + +export const schema = { + 'foo.total': { accumulate: true }, + 'foo.inc': {}, + 'foo.dec': {}, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/entities/users.ts b/packages/backend/src/core/chart/charts/entities/users.ts new file mode 100644 index 0000000000..c0b83094ae --- /dev/null +++ b/packages/backend/src/core/chart/charts/entities/users.ts @@ -0,0 +1,14 @@ +import Chart from '../../core.js'; + +export const name = 'users'; + +export const schema = { + 'local.total': { accumulate: true }, + 'local.inc': { range: 'small' }, + 'local.dec': { range: 'small' }, + 'remote.total': { accumulate: true }, + 'remote.inc': { range: 'small' }, + 'remote.dec': { range: 'small' }, +} as const; + +export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts new file mode 100644 index 0000000000..372e0f1fae --- /dev/null +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -0,0 +1,121 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/federation.js'; +import type { KVs } from '../core.js'; + +/** + * フェデレーションに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class FederationChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private metaService: MetaService, + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return { + }; + } + + protected async tickMinor(): Promise>> { + const meta = await this.metaService.fetch(); + + const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') + .select('instance.host') + .where('instance.isSuspended = true'); + + const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f') + .select('f.followerHost') + .where('f.followerHost IS NOT NULL'); + + const subInstancesQuery = this.followingsRepository.createQueryBuilder('f') + .select('f.followeeHost') + .where('f.followeeHost IS NOT NULL'); + + const pubInstancesQuery = this.followingsRepository.createQueryBuilder('f') + .select('f.followerHost') + .where('f.followerHost IS NOT NULL'); + + const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([ + this.followingsRepository.createQueryBuilder('following') + .select('COUNT(DISTINCT following.followeeHost)') + .where('following.followeeHost IS NOT NULL') + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) + .getRawOne() + .then(x => parseInt(x.count, 10)), + this.followingsRepository.createQueryBuilder('following') + .select('COUNT(DISTINCT following.followerHost)') + .where('following.followerHost IS NOT NULL') + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) + .getRawOne() + .then(x => parseInt(x.count, 10)), + this.followingsRepository.createQueryBuilder('following') + .select('COUNT(DISTINCT following.followeeHost)') + .where('following.followeeHost IS NOT NULL') + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) + .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) + .setParameters(pubsubSubQuery.getParameters()) + .getRawOne() + .then(x => parseInt(x.count, 10)), + this.instancesRepository.createQueryBuilder('instance') + .select('COUNT(instance.id)') + .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere('instance.isSuspended = false') + .andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) + .getRawOne() + .then(x => parseInt(x.count, 10)), + this.instancesRepository.createQueryBuilder('instance') + .select('COUNT(instance.id)') + .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere('instance.isSuspended = false') + .andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) + .getRawOne() + .then(x => parseInt(x.count, 10)), + ]); + + return { + 'sub': sub, + 'pub': pub, + 'pubsub': pubsub, + 'subActive': subActive, + 'pubActive': pubActive, + }; + } + + public async deliverd(host: string, succeeded: boolean): Promise { + await this.commit(succeeded ? { + 'deliveredInstances': [host], + } : { + 'stalled': [host], + }); + } + + 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 new file mode 100644 index 0000000000..66ac0b882b --- /dev/null +++ b/packages/backend/src/core/chart/charts/hashtag.ts @@ -0,0 +1,41 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +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 Chart from '../core.js'; +import { name, schema } from './entities/hashtag.js'; +import type { KVs } from '../core.js'; + +/** + * ハッシュタグに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class HashtagChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + private userEntityService: UserEntityService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise { + await this.commit({ + 'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [], + 'remote.users': this.userEntityService.isLocalUser(user) ? [] : [user.id], + }, hashtag); + } +} diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts new file mode 100644 index 0000000000..c43ebeddc1 --- /dev/null +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -0,0 +1,127 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +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 Chart from '../core.js'; +import { name, schema } from './entities/instance.js'; +import type { KVs } from '../core.js'; + +/** + * インスタンスごとのチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class InstanceChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private utilityService: UtilityService, + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + const [ + notesCount, + usersCount, + followingCount, + followersCount, + driveFiles, + ] = await Promise.all([ + this.notesRepository.countBy({ userHost: group }), + this.usersRepository.countBy({ host: group }), + this.followingsRepository.countBy({ followerHost: group }), + this.followingsRepository.countBy({ followeeHost: group }), + this.driveFilesRepository.countBy({ userHost: group }), + ]); + + return { + 'notes.total': notesCount, + 'users.total': usersCount, + 'following.total': followingCount, + 'followers.total': followersCount, + 'drive.totalFiles': driveFiles, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async requestReceived(host: string): Promise { + await this.commit({ + 'requests.received': 1, + }, this.utilityService.toPuny(host)); + } + + public async requestSent(host: string, isSucceeded: boolean): Promise { + await this.commit({ + 'requests.succeeded': isSucceeded ? 1 : 0, + 'requests.failed': isSucceeded ? 0 : 1, + }, this.utilityService.toPuny(host)); + } + + public async newUser(host: string): Promise { + await this.commit({ + 'users.total': 1, + 'users.inc': 1, + }, this.utilityService.toPuny(host)); + } + + public async updateNote(host: string, note: Note, isAdditional: boolean): Promise { + await this.commit({ + 'notes.total': isAdditional ? 1 : -1, + 'notes.inc': isAdditional ? 1 : 0, + 'notes.dec': isAdditional ? 0 : 1, + 'notes.diffs.normal': note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, + 'notes.diffs.renote': note.renoteId != null ? (isAdditional ? 1 : -1) : 0, + 'notes.diffs.reply': note.replyId != null ? (isAdditional ? 1 : -1) : 0, + 'notes.diffs.withFile': note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, + }, this.utilityService.toPuny(host)); + } + + public async updateFollowing(host: string, isAdditional: boolean): Promise { + await this.commit({ + 'following.total': isAdditional ? 1 : -1, + 'following.inc': isAdditional ? 1 : 0, + 'following.dec': isAdditional ? 0 : 1, + }, this.utilityService.toPuny(host)); + } + + public async updateFollowers(host: string, isAdditional: boolean): Promise { + await this.commit({ + 'followers.total': isAdditional ? 1 : -1, + 'followers.inc': isAdditional ? 1 : 0, + 'followers.dec': isAdditional ? 0 : 1, + }, this.utilityService.toPuny(host)); + } + + public async updateDrive(file: DriveFile, isAdditional: boolean): Promise { + const fileSizeKb = file.size / 1000; + await this.commit({ + 'drive.totalFiles': isAdditional ? 1 : -1, + 'drive.incFiles': isAdditional ? 1 : 0, + 'drive.incUsage': isAdditional ? fileSizeKb : 0, + 'drive.decFiles': isAdditional ? 1 : 0, + 'drive.decUsage': isAdditional ? fileSizeKb : 0, + }, file.userHost); + } +} diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts new file mode 100644 index 0000000000..1597b5727e --- /dev/null +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -0,0 +1,58 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Not, IsNull, DataSource } from 'typeorm'; +import { 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 Chart from '../core.js'; +import { name, schema } from './entities/notes.js'; +import type { KVs } from '../core.js'; + +/** + * ノートに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class NotesChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + const [localCount, remoteCount] = await Promise.all([ + this.notesRepository.countBy({ userHost: IsNull() }), + this.notesRepository.countBy({ userHost: Not(IsNull()) }), + ]); + + return { + 'local.total': localCount, + 'remote.total': remoteCount, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(note: Note, isAdditional: boolean): Promise { + const prefix = note.userHost === null ? 'local' : 'remote'; + + await this.commit({ + [`${prefix}.total`]: isAdditional ? 1 : -1, + [`${prefix}.inc`]: isAdditional ? 1 : 0, + [`${prefix}.dec`]: isAdditional ? 0 : 1, + [`${prefix}.diffs.normal`]: note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, + [`${prefix}.diffs.renote`]: note.renoteId != null ? (isAdditional ? 1 : -1) : 0, + [`${prefix}.diffs.reply`]: note.replyId != null ? (isAdditional ? 1 : -1) : 0, + [`${prefix}.diffs.withFile`]: note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, + }); + } +} diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts new file mode 100644 index 0000000000..181b9a38bb --- /dev/null +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -0,0 +1,58 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { DriveFilesRepository } from '@/models/index.js'; +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 Chart from '../core.js'; +import { name, schema } from './entities/per-user-drive.js'; +import type { KVs } from '../core.js'; + +/** + * ユーザーごとのドライブに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class PerUserDriveChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private appLockService: AppLockService, + private driveFileEntityService: DriveFileEntityService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + const [count, size] = await Promise.all([ + this.driveFilesRepository.countBy({ userId: group }), + this.driveFileEntityService.calcDriveUsageOf(group), + ]); + + return { + 'totalCount': count, + 'totalSize': size, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(file: DriveFile, isAdditional: boolean): Promise { + const fileSizeKb = file.size / 1000; + await this.commit({ + 'totalCount': isAdditional ? 1 : -1, + 'totalSize': isAdditional ? fileSizeKb : -fileSizeKb, + 'incCount': isAdditional ? 1 : 0, + 'incSize': isAdditional ? fileSizeKb : 0, + 'decCount': isAdditional ? 0 : 1, + 'decSize': isAdditional ? 0 : fileSizeKb, + }, file.userId); + } +} diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts new file mode 100644 index 0000000000..5195723a25 --- /dev/null +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -0,0 +1,71 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Not, IsNull, DataSource } from 'typeorm'; +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 { FollowingsRepository } from '@/models/index.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/per-user-following.js'; +import type { KVs } from '../core.js'; + +/** + * ユーザーごとのフォローに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class PerUserFollowingChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private appLockService: AppLockService, + private userEntityService: UserEntityService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + const [ + localFollowingsCount, + localFollowersCount, + remoteFollowingsCount, + remoteFollowersCount, + ] = await Promise.all([ + this.followingsRepository.countBy({ followerId: group, followeeHost: IsNull() }), + this.followingsRepository.countBy({ followeeId: group, followerHost: IsNull() }), + this.followingsRepository.countBy({ followerId: group, followeeHost: Not(IsNull()) }), + this.followingsRepository.countBy({ followeeId: group, followerHost: Not(IsNull()) }), + ]); + + return { + 'local.followings.total': localFollowingsCount, + 'local.followers.total': localFollowersCount, + 'remote.followings.total': remoteFollowingsCount, + 'remote.followers.total': remoteFollowersCount, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + 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'; + + this.commit({ + [`${prefixFollower}.followings.total`]: isFollow ? 1 : -1, + [`${prefixFollower}.followings.inc`]: isFollow ? 1 : 0, + [`${prefixFollower}.followings.dec`]: isFollow ? 0 : 1, + }, follower.id); + this.commit({ + [`${prefixFollowee}.followers.total`]: isFollow ? 1 : -1, + [`${prefixFollowee}.followers.inc`]: isFollow ? 1 : 0, + [`${prefixFollowee}.followers.dec`]: isFollow ? 0 : 1, + }, followee.id); + } +} diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts new file mode 100644 index 0000000000..6dbe309b7c --- /dev/null +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -0,0 +1,55 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository } from '@/models/index.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/per-user-notes.js'; +import type { KVs } from '../core.js'; + +/** + * ユーザーごとのノートに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class PerUserNotesChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + const [count] = await Promise.all([ + this.notesRepository.countBy({ userId: group }), + ]); + + return { + total: count, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise { + await this.commit({ + 'total': isAdditional ? 1 : -1, + 'inc': isAdditional ? 1 : 0, + 'dec': isAdditional ? 0 : 1, + 'diffs.normal': note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, + 'diffs.renote': note.renoteId != null ? (isAdditional ? 1 : -1) : 0, + 'diffs.reply': note.replyId != null ? (isAdditional ? 1 : -1) : 0, + 'diffs.withFile': note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, + }, user.id); + } +} diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts new file mode 100644 index 0000000000..73a58656f8 --- /dev/null +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -0,0 +1,42 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import type { User } from '@/models/entities/User.js'; +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 Chart from '../core.js'; +import { name, schema } from './entities/per-user-reactions.js'; +import type { KVs } from '../core.js'; + +/** + * ユーザーごとのリアクションに関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class PerUserReactionsChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + private userEntityService: UserEntityService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise { + const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; + this.commit({ + [`${prefix}.count`]: 1, + }, note.userId); + } +} diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts new file mode 100644 index 0000000000..e6cbe89790 --- /dev/null +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -0,0 +1,46 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/test-grouped.js'; +import type { KVs } from '../core.js'; + +/** + * For testing + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class TestGroupedChart extends Chart { + private total = {} as Record; + + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema, true); + } + + protected async tickMajor(group: string): Promise>> { + return { + 'foo.total': this.total[group], + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async increment(group: string): Promise { + if (this.total[group] == null) this.total[group] = 0; + + this.total[group]++; + + await this.commit({ + 'foo.total': 1, + 'foo.inc': 1, + }, group); + } +} diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts new file mode 100644 index 0000000000..f2f17c8de6 --- /dev/null +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -0,0 +1,43 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/test-intersection.js'; +import type { KVs } from '../core.js'; + +/** + * For testing + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class TestIntersectionChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async addA(key: string): Promise { + await this.commit({ + a: [key], + }); + } + + 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 new file mode 100644 index 0000000000..ce01594520 --- /dev/null +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -0,0 +1,37 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/test-unique.js'; +import type { KVs } from '../core.js'; + +/** + * For testing + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class TestUniqueChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return {}; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + 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 new file mode 100644 index 0000000000..bd59b7aa63 --- /dev/null +++ b/packages/backend/src/core/chart/charts/test.ts @@ -0,0 +1,53 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AppLockService } from '@/core/AppLockService.js'; +import { DI } from '@/di-symbols.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/test.js'; +import type { KVs } from '../core.js'; + +/** + * For testing + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class TestChart extends Chart { + public total = 0; // publicにするのはテストのため + + constructor( + @Inject(DI.db) + private db: DataSource, + + private appLockService: AppLockService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + return { + 'foo.total': this.total, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async increment(): Promise { + this.total++; + + await this.commit({ + 'foo.total': 1, + 'foo.inc': 1, + }); + } + + public async decrement(): Promise { + this.total--; + + await this.commit({ + 'foo.total': -1, + 'foo.dec': 1, + }); + } +} diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts new file mode 100644 index 0000000000..4fdddcc0a3 --- /dev/null +++ b/packages/backend/src/core/chart/charts/users.ts @@ -0,0 +1,56 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Not, IsNull, DataSource } from 'typeorm'; +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 { UsersRepository } from '@/models/index.js'; +import Chart from '../core.js'; +import { name, schema } from './entities/users.js'; +import type { KVs } from '../core.js'; + +/** + * ユーザー数に関するチャート + */ +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class UsersChart extends Chart { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private appLockService: AppLockService, + private userEntityService: UserEntityService, + ) { + super(db, (k) => appLockService.getChartInsertLock(k), name, schema); + } + + protected async tickMajor(): Promise>> { + const [localCount, remoteCount] = await Promise.all([ + this.usersRepository.countBy({ host: IsNull() }), + this.usersRepository.countBy({ host: Not(IsNull()) }), + ]); + + return { + 'local.total': localCount, + 'remote.total': remoteCount, + }; + } + + protected async tickMinor(): Promise>> { + return {}; + } + + public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise { + const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; + + await this.commit({ + [`${prefix}.total`]: isAdditional ? 1 : -1, + [`${prefix}.inc`]: isAdditional ? 1 : 0, + [`${prefix}.dec`]: isAdditional ? 0 : 1, + }); + } +} diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts new file mode 100644 index 0000000000..1933e80c7b --- /dev/null +++ b/packages/backend/src/core/chart/core.ts @@ -0,0 +1,679 @@ +/** + * チャートエンジン + * + * Tests located in test/chart + */ + +import * as nestedProperty from 'nested-property'; +import { EntitySchema, LessThan, Between } from 'typeorm'; +import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js'; +import Logger from '@/logger.js'; +import type { Repository, DataSource } from 'typeorm'; + +const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); + +const columnPrefix = '___' as const; +const uniqueTempColumnPrefix = 'unique_temp___' as const; +const columnDot = '_' as const; + +type Schema = Record; + + range?: 'big' | 'small' | 'medium'; + + // previousな値を引き継ぐかどうか + accumulate?: boolean; +}>; + +type KeyToColumnName = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof columnDot}${KeyToColumnName}` : T; + +type Columns = { + [K in keyof S as `${typeof columnPrefix}${KeyToColumnName}`]: number; +}; + +type TempColumnsForUnique = { + [K in keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName}`]: S[K]['uniqueIncrement'] extends true ? string[] : never; +}; + +type RawRecord = { + id: number; + + /** + * 集計のグループ + */ + group?: string | null; + + /** + * 集計日時のUnixタイムスタンプ(秒) + */ + date: number; +} & TempColumnsForUnique & Columns; + +const camelToSnake = (str: string): string => { + return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); +}; + +const removeDuplicates = (array: any[]) => Array.from(new Set(array)); + +type Commit = { + [K in keyof S]?: S[K]['uniqueIncrement'] extends true ? string[] : number; +}; + +export type KVs = { + [K in keyof S]: number; +}; + +type ChartResult = { + [P in keyof T]: number[]; +}; + +type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; + +type UnflattenSingleton = K extends `${infer A}.${infer B}` + ? { [_ in A]: UnflattenSingleton; } + : { [_ in K]: V; }; + +type Unflatten> = UnionToIntersection< + { + [K in Extract]: UnflattenSingleton; + }[Extract] +>; + +type ToJsonSchema = { + type: 'object'; + properties: { + [K in keyof S]: S[K] extends number[] ? { type: 'array'; items: { type: 'number'; }; } : ToJsonSchema; + }, + required: (keyof S)[]; +}; + +export function getJsonSchema(schema: S): ToJsonSchema>> { + const jsonSchema = { + type: 'object', + properties: {} as Record, + required: [], + }; + + for (const k in schema) { + jsonSchema.properties[k] = { + type: 'array', + items: { type: 'number' }, + }; + } + + return jsonSchema as ToJsonSchema>>; +} + +/** + * 様々なチャートの管理を司るクラス + */ +// eslint-disable-next-line import/no-default-export +export default abstract class Chart { + public schema: T; + + private name: string; + private buffer: { + diff: Commit; + group: string | null; + }[] = []; + // ↓にしたいけどfindOneとかで型エラーになる + //private repositoryForHour: Repository>; + //private repositoryForDay: Repository>; + private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; + private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; + + /** + * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) + */ + protected abstract tickMajor(group: string | null): Promise>>; + + /** + * 少なくとも最小スパン内に1回は実行されて欲しい計算処理を入れる + */ + protected abstract tickMinor(group: string | null): Promise>>; + + private static convertSchemaToColumnDefinitions(schema: Schema): Record { + const columns = {} as Record; + for (const [k, v] of Object.entries(schema)) { + const name = k.replaceAll('.', columnDot); + const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer'; + if (v.uniqueIncrement) { + columns[uniqueTempColumnPrefix + name] = { + type: 'varchar', + array: true, + default: '{}', + }; + columns[columnPrefix + name] = { + type, + default: 0, + }; + } else { + columns[columnPrefix + name] = { + type, + default: 0, + }; + } + } + return columns; + } + + private static dateToTimestamp(x: Date): number { + return Math.floor(x.getTime() / 1000); + } + + private static parseDate(date: Date): [number, number, number, number, number, number, number] { + const y = date.getUTCFullYear(); + const m = date.getUTCMonth(); + const d = date.getUTCDate(); + const h = date.getUTCHours(); + const _m = date.getUTCMinutes(); + const _s = date.getUTCSeconds(); + const _ms = date.getUTCMilliseconds(); + + return [y, m, d, h, _m, _s, _ms]; + } + + private static getCurrentDate() { + return Chart.parseDate(new Date()); + } + + public static schemaToEntity(name: string, schema: Schema, grouped = false): { + hour: EntitySchema, + day: EntitySchema, + } { + const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({ + name: + span === 'hour' ? `__chart__${camelToSnake(name)}` : + span === 'day' ? `__chart_day__${camelToSnake(name)}` : + new Error('not happen') as never, + columns: { + id: { + type: 'integer', + primary: true, + generated: true, + }, + date: { + type: 'integer', + }, + ...(grouped ? { + group: { + type: 'varchar', + length: 128, + }, + } : {}), + ...Chart.convertSchemaToColumnDefinitions(schema), + }, + indices: [{ + columns: grouped ? ['date', 'group'] : ['date'], + unique: true, + }], + uniques: [{ + columns: grouped ? ['date', 'group'] : ['date'], + }], + relations: { + /* TODO + group: { + target: () => Foo, + type: 'many-to-one', + onDelete: 'CASCADE', + }, + */ + }, + }); + + return { + hour: createEntity('hour'), + day: createEntity('day'), + }; + } + + private lock: (key: string) => Promise<() => void>; + + constructor(db: DataSource, lock: (key: string) => Promise<() => void>, name: string, schema: T, grouped = false) { + this.name = name; + this.schema = schema; + this.lock = lock; + + const { hour, day } = Chart.schemaToEntity(name, schema, grouped); + this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); + this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); + } + + private convertRawRecord(x: RawRecord): KVs { + const kvs = {} as Record; + for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns)[]) { + kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k] as unknown as number; + } + return kvs as KVs; + } + + 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]])[]) { + if (v.accumulate && latest) { + log[k] = latest[k]; + } else { + log[k] = 0; + } + } + return log as KVs; + } + + private getLatestLog(group: string | null, span: 'hour' | 'day'): Promise | null> { + const repository = + span === 'hour' ? this.repositoryForHour : + span === 'day' ? this.repositoryForDay : + new Error('not happen') as never; + + return repository.findOne({ + where: group ? { + group: group, + } : {}, + order: { + date: -1, + }, + }).then(x => x ?? null) as Promise | null>; + } + + /** + * 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。 + */ + private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { + const [y, m, d, h] = Chart.getCurrentDate(); + + const current = dateUTC( + span === 'hour' ? [y, m, d, h] : + span === 'day' ? [y, m, d] : + new Error('not happen') as never); + + const repository = + span === 'hour' ? this.repositoryForHour : + span === 'day' ? this.repositoryForDay : + new Error('not happen') as never; + + // 現在(=今のHour or Day)のログ + const currentLog = await repository.findOneBy({ + date: Chart.dateToTimestamp(current), + ...(group ? { group: group } : {}), + }) as RawRecord | undefined; + + // ログがあればそれを返して終了 + if (currentLog != null) { + return currentLog; + } + + let log: RawRecord; + let data: KVs; + + // 集計期間が変わってから、初めてのチャート更新なら + // 最も最近のログを持ってくる + // * 例えば集計期間が「日」である場合で考えると、 + // * 昨日何もチャートを更新するような出来事がなかった場合は、 + // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 + // * 「昨日の」と決め打ちせずに「もっとも最近の」とします + const latest = await this.getLatestLog(group, span); + + if (latest != null) { + // 空ログデータを作成 + data = this.getNewLog(this.convertRawRecord(latest)); + } else { + // ログが存在しなかったら + // (Misskeyインスタンスを建てて初めてのチャート更新時など) + + // 初期ログデータを作成 + data = this.getNewLog(null); + + logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); + } + + const date = Chart.dateToTimestamp(current); + const lockKey = group ? `${this.name}:${date}:${span}:${group}` : `${this.name}:${date}:${span}`; + + const unlock = await this.lock(lockKey); + try { + // ロック内でもう1回チェックする + const currentLog = await repository.findOneBy({ + date: date, + ...(group ? { group: group } : {}), + }) as RawRecord | undefined; + + // ログがあればそれを返して終了 + if (currentLog != null) return currentLog; + + const columns = {} as Record; + for (const [k, v] of Object.entries(data)) { + const name = k.replaceAll('.', columnDot); + columns[columnPrefix + name] = v; + } + + // 新規ログ挿入 + log = await repository.insert({ + date: date, + ...(group ? { group: group } : {}), + ...columns, + }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord; + + logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); + + return log; + } finally { + unlock(); + } + } + + protected commit(diff: Commit, group: string | null = null): void { + for (const [k, v] of Object.entries(diff)) { + if (v == null || v === 0 || (Array.isArray(v) && v.length === 0)) delete diff[k]; + } + this.buffer.push({ + diff, group, + }); + } + + public async save(): Promise { + if (this.buffer.length === 0) { + logger.info(`${this.name}: Write skipped`); + return; + } + + // TODO: 前の時間のログがbufferにあった場合のハンドリング + // 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。 + // 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、 + // そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。 + // これを回避するための実装は複雑になりそうなため、一旦保留。 + + const update = async (logHour: RawRecord, logDay: RawRecord): Promise => { + const finalDiffs = {} as Record; + + for (const diff of this.buffer.filter(q => q.group == null || (q.group === logHour.group)).map(q => q.diff)) { + for (const [k, v] of Object.entries(diff)) { + if (finalDiffs[k] == null) { + finalDiffs[k] = v; + } else { + if (typeof finalDiffs[k] === 'number') { + (finalDiffs[k] as number) += v as number; + } else { + (finalDiffs[k] as string[]) = (finalDiffs[k] as string[]).concat(v); + } + } + } + } + + const queryForHour: Record, number | (() => string)> = {} as any; + const queryForDay: Record, number | (() => string)> = {} as any; + for (const [k, v] of Object.entries(finalDiffs)) { + if (typeof v === 'number') { + const name = columnPrefix + k.replaceAll('.', columnDot) as string & keyof Columns; + if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`; + if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`; + if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`; + if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`; + } else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント + const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as string & keyof TempColumnsForUnique; + // TODO: item をSQLエスケープ + const itemsForHour = v.filter(item => !(logHour[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`); + const itemsForDay = v.filter(item => !(logDay[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`); + if (itemsForHour.length > 0) queryForHour[tempColumnName] = () => `array_cat("${tempColumnName}", '{${itemsForHour.join(',')}}'::varchar[])`; + if (itemsForDay.length > 0) queryForDay[tempColumnName] = () => `array_cat("${tempColumnName}", '{${itemsForDay.join(',')}}'::varchar[])`; + } + } + + // bake unique count + for (const [k, v] of Object.entries(finalDiffs)) { + if (this.schema[k].uniqueIncrement) { + const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns; + const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; + queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size; + queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size; + } + } + + // compute intersection + // TODO: intersectionに指定されたカラムがintersectionだった場合の対応 + for (const [k, v] of Object.entries(this.schema)) { + const intersection = v.intersection; + if (intersection) { + const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns; + const firstKey = intersection[0]; + const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique; + const firstValues = finalDiffs[firstKey] as string[] | undefined; + const currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]); + const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]); + for (let i = 1; i < intersection.length; i++) { + const targetKey = intersection[i]; + const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique; + const targetValues = finalDiffs[targetKey] as string[] | undefined; + const targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[targetTempColumnName] as unknown as string[])]); + const targetValuesForDay = new Set([...(targetValues ?? []), ...(logDay[targetTempColumnName] as unknown as string[])]); + currentValuesForHour.forEach(v => { + if (!targetValuesForHour.has(v)) currentValuesForHour.delete(v); + }); + currentValuesForDay.forEach(v => { + if (!targetValuesForDay.has(v)) currentValuesForDay.delete(v); + }); + } + queryForHour[name] = currentValuesForHour.size; + queryForDay[name] = currentValuesForDay.size; + } + } + + // ログ更新 + await Promise.all([ + this.repositoryForHour.createQueryBuilder() + .update() + .set(queryForHour as any) + .where('id = :id', { id: logHour.id }) + .execute(), + this.repositoryForDay.createQueryBuilder() + .update() + .set(queryForDay as any) + .where('id = :id', { id: logDay.id }) + .execute(), + ]); + + logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); + + // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする + this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); + }; + + const groups = removeDuplicates(this.buffer.map(log => log.group)); + + await Promise.all( + groups.map(group => + Promise.all([ + this.claimCurrentLog(group, 'hour'), + this.claimCurrentLog(group, 'day'), + ]).then(([logHour, logDay]) => + update(logHour, logDay)))); + } + + public async tick(major: boolean, group: string | null = null): Promise { + const data = major ? await this.tickMajor(group) : await this.tickMinor(group); + + const columns = {} as Record, number>; + for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) { + const name = columnPrefix + (k as string).replaceAll('.', columnDot) as keyof Columns; + columns[name] = v; + } + + if (Object.keys(columns).length === 0) { + return; + } + + const update = async (logHour: RawRecord, logDay: RawRecord): Promise => { + await Promise.all([ + this.repositoryForHour.createQueryBuilder() + .update() + .set(columns) + .where('id = :id', { id: logHour.id }) + .execute(), + this.repositoryForDay.createQueryBuilder() + .update() + .set(columns) + .where('id = :id', { id: logDay.id }) + .execute(), + ]); + }; + + return Promise.all([ + this.claimCurrentLog(group, 'hour'), + this.claimCurrentLog(group, 'day'), + ]).then(([logHour, logDay]) => + update(logHour, logDay)); + } + + public resync(group: string | null = null): Promise { + return this.tick(true, group); + } + + public async clean(): Promise { + const current = dateUTC(Chart.getCurrentDate()); + + // 一日以上前かつ三日以内 + const gt = Chart.dateToTimestamp(current) - (60 * 60 * 24 * 3); + const lt = Chart.dateToTimestamp(current) - (60 * 60 * 24); + + const columns = {} as Record, []>; + for (const [k, v] of Object.entries(this.schema)) { + if (v.uniqueIncrement) { + const name = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; + columns[name] = []; + } + } + + if (Object.keys(columns).length === 0) { + return; + } + + await Promise.all([ + this.repositoryForHour.createQueryBuilder() + .update() + .set(columns) + .where('date > :gt', { gt }) + .andWhere('date < :lt', { lt }) + .execute(), + this.repositoryForDay.createQueryBuilder() + .update() + .set(columns) + .where('date > :gt', { gt }) + .andWhere('date < :lt', { lt }) + .execute(), + ]); + } + + 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; + + const lt = dateUTC([y, m, d, h, _m, _s, _ms]); + + const gt = + span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : + span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : + new Error('not happen') as never; + + const repository = + span === 'hour' ? this.repositoryForHour : + span === 'day' ? this.repositoryForDay : + new Error('not happen') as never; + + // ログ取得 + let logs = await repository.find({ + where: { + date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)), + ...(group ? { group: group } : {}), + }, + order: { + date: -1, + }, + }) as RawRecord[]; + + // 要求された範囲にログがひとつもなかったら + if (logs.length === 0) { + // もっとも新しいログを持ってくる + // (すくなくともひとつログが無いと隙間埋めできないため) + const recentLog = await repository.findOne({ + where: group ? { + group: group, + } : {}, + order: { + date: -1, + }, + }) as RawRecord | undefined; + + if (recentLog) { + logs = [recentLog]; + } + + // 要求された範囲の最も古い箇所に位置するログが存在しなかったら + } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { + // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する + // (隙間埋めできないため) + const outdatedLog = await repository.findOne({ + where: { + date: LessThan(Chart.dateToTimestamp(gt)), + ...(group ? { group: group } : {}), + }, + order: { + date: -1, + }, + }) as RawRecord | undefined; + + if (outdatedLog) { + logs.push(outdatedLog); + } + } + + const chart: KVs[] = []; + + for (let i = (amount - 1); i >= 0; i--) { + const current = + span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') : + span === 'day' ? subtractTime(dateUTC([y, m, d]), i, 'day') : + new Error('not happen') as never; + + const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); + + if (log) { + chart.unshift(this.convertRawRecord(log)); + } else { + // 隙間埋め + const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); + const data = latest ? this.convertRawRecord(latest) : null; + chart.unshift(this.getNewLog(data)); + } + } + + const res = {} as ChartResult; + + /** + * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] + * を + * { foo: [1, 2, 3], bar: [5, 6, 7] } + * にする + */ + for (const record of chart) { + for (const [k, v] of Object.entries(record) as ([keyof typeof record, number])[]) { + if (res[k]) { + res[k].push(v); + } else { + res[k] = [v]; + } + } + } + + return res; + } + + 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 = {}; + for (const [k, v] of Object.entries(result)) { + nestedProperty.set(object, k, v); + } + return object as Unflatten>; + } +} diff --git a/packages/backend/src/core/chart/entities.ts b/packages/backend/src/core/chart/entities.ts new file mode 100644 index 0000000000..a9eeabd639 --- /dev/null +++ b/packages/backend/src/core/chart/entities.ts @@ -0,0 +1,39 @@ +import { entity as FederationChart } from './charts/entities/federation.js'; +import { entity as NotesChart } from './charts/entities/notes.js'; +import { entity as UsersChart } from './charts/entities/users.js'; +import { entity as ActiveUsersChart } from './charts/entities/active-users.js'; +import { entity as InstanceChart } from './charts/entities/instance.js'; +import { entity as PerUserNotesChart } from './charts/entities/per-user-notes.js'; +import { entity as DriveChart } from './charts/entities/drive.js'; +import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js'; +import { entity as HashtagChart } from './charts/entities/hashtag.js'; +import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js'; +import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; +import { entity as ApRequestChart } from './charts/entities/ap-request.js'; + +import { entity as TestChart } from './charts/entities/test.js'; +import { entity as TestGroupedChart } from './charts/entities/test-grouped.js'; +import { entity as TestUniqueChart } from './charts/entities/test-unique.js'; +import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js'; + +export const entities = [ + FederationChart.hour, FederationChart.day, + NotesChart.hour, NotesChart.day, + UsersChart.hour, UsersChart.day, + ActiveUsersChart.hour, ActiveUsersChart.day, + InstanceChart.hour, InstanceChart.day, + PerUserNotesChart.hour, PerUserNotesChart.day, + DriveChart.hour, DriveChart.day, + PerUserReactionsChart.hour, PerUserReactionsChart.day, + HashtagChart.hour, HashtagChart.day, + PerUserFollowingChart.hour, PerUserFollowingChart.day, + PerUserDriveChart.hour, PerUserDriveChart.day, + ApRequestChart.hour, ApRequestChart.day, + + ...(process.env.NODE_ENV === 'test' ? [ + TestChart.hour, TestChart.day, + TestGroupedChart.hour, TestGroupedChart.day, + TestUniqueChart.hour, TestUniqueChart.day, + TestIntersectionChart.hour, TestIntersectionChart.day, + ] : []), +]; diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts new file mode 100644 index 0000000000..6cc511fb48 --- /dev/null +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { AbuseUserReportsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class AbuseUserReportEntityService { + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: AbuseUserReport['id'] | AbuseUserReport, + ) { + const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: report.id, + createdAt: report.createdAt.toISOString(), + comment: report.comment, + resolved: report.resolved, + reporterId: report.reporterId, + targetUserId: report.targetUserId, + assigneeId: report.assigneeId, + reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, { + detail: true, + }), + targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { + detail: true, + }), + assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { + detail: true, + }) : null, + forwarded: report.forwarded, + }); + } + + public packMany( + reports: any[], + ) { + return Promise.all(reports.map(x => this.pack(x))); + } +} diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts new file mode 100644 index 0000000000..9193cb81d7 --- /dev/null +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; + +@Injectable() +export class AntennaEntityService { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + ) { + } + + public async pack( + src: Antenna['id'] | Antenna, + ): Promise> { + const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); + + const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null; + const userGroupJoining = antenna.userGroupJoiningId ? await this.userGroupJoiningsRepository.findOneBy({ id: antenna.userGroupJoiningId }) : null; + + return { + id: antenna.id, + createdAt: antenna.createdAt.toISOString(), + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, + users: antenna.users, + caseSensitive: antenna.caseSensitive, + notify: antenna.notify, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + hasUnreadNote, + }; + } +} diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts new file mode 100644 index 0000000000..6491b0b2d9 --- /dev/null +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { AccessTokensRepository, AppsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { App } from '@/models/entities/App.js'; +import type { User } from '@/models/entities/User.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class AppEntityService { + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + ) { + } + + public async pack( + src: App['id'] | App, + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: boolean, + includeSecret?: boolean, + includeProfileImageIds?: boolean + }, + ): Promise> { + const opts = Object.assign({ + detail: false, + includeSecret: false, + includeProfileImageIds: false, + }, options); + + const app = typeof src === 'object' ? src : await this.appsRepository.findOneByOrFail({ id: src }); + + return { + id: app.id, + name: app.name, + callbackUrl: app.callbackUrl, + permission: app.permission, + ...(opts.includeSecret ? { secret: app.secret } : {}), + ...(me ? { + isAuthorized: await this.accessTokensRepository.countBy({ + appId: app.id, + userId: me.id, + }).then(count => count > 0), + } : {}), + }; + } +} diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts new file mode 100644 index 0000000000..a4dab3d9cf --- /dev/null +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { AuthSessionsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { AuthSession } from '@/models/entities/AuthSession.js'; +import type { User } from '@/models/entities/User.js'; +import { UserEntityService } from './UserEntityService.js'; +import { AppEntityService } from './AppEntityService.js'; + +@Injectable() +export class AuthSessionEntityService { + constructor( + @Inject(DI.authSessionsRepository) + private authSessionsRepository: AuthSessionsRepository, + + private appEntityService: AppEntityService, + ) { + } + + public async pack( + src: AuthSession['id'] | AuthSession, + me?: { id: User['id'] } | null | undefined, + ) { + const session = typeof src === 'object' ? src : await this.authSessionsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: session.id, + app: this.appEntityService.pack(session.appId, me), + token: session.token, + }); + } +} diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts new file mode 100644 index 0000000000..74ce6830b6 --- /dev/null +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { BlockingsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class BlockingEntityService { + constructor( + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Blocking['id'] | Blocking, + me?: { id: User['id'] } | null | undefined, + ): Promise> { + const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: blocking.id, + createdAt: blocking.createdAt.toISOString(), + blockeeId: blocking.blockeeId, + blockee: this.userEntityService.pack(blocking.blockeeId, me, { + detail: true, + }), + }); + } + + public packMany( + blockings: any[], + me: { id: User['id'] }, + ) { + return Promise.all(blockings.map(x => this.pack(x, me))); + } +} diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts new file mode 100644 index 0000000000..fec76e4e63 --- /dev/null +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -0,0 +1,66 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; + +@Injectable() +export class ChannelEntityService { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + ) { + } + + public async pack( + src: Channel['id'] | Channel, + me?: { id: User['id'] } | null | undefined, + ): Promise> { + const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src }); + const meId = me ? me.id : null; + + const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; + + const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined; + + const following = meId ? await this.channelFollowingsRepository.findOneBy({ + followerId: meId, + followeeId: channel.id, + }) : null; + + return { + id: channel.id, + createdAt: channel.createdAt.toISOString(), + lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null, + name: channel.name, + description: channel.description, + userId: channel.userId, + bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null, + usersCount: channel.usersCount, + notesCount: channel.notesCount, + + ...(me ? { + isFollowing: following != null, + hasUnreadNote, + } : {}), + }; + } +} + diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts new file mode 100644 index 0000000000..27637e42e8 --- /dev/null +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -0,0 +1,43 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { ClipsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Clip } from '@/models/entities/Clip.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ClipEntityService { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Clip['id'] | Clip, + ): Promise> { + const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: clip.id, + createdAt: clip.createdAt.toISOString(), + userId: clip.userId, + user: this.userEntityService.pack(clip.user ?? clip.userId), + name: clip.name, + description: clip.description, + isPublic: clip.isPublic, + }); + } + + public packMany( + clips: Clip[], + ) { + return Promise.all(clips.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts new file mode 100644 index 0000000000..521bf51da0 --- /dev/null +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -0,0 +1,214 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import * as mfm from 'mfm-js'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { Packed } from '@/misc/schema.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { User } from '@/models/entities/User.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { appendQuery, query } from '@/misc/prelude/url.js'; +import { UtilityService } from '../UtilityService.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFolderEntityService } from './DriveFolderEntityService.js'; + +type PackOptions = { + detail?: boolean, + self?: boolean, + withUser?: boolean, +}; + +@Injectable() +export class DriveFileEntityService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + // 循環参照のため / for circular dependency + @Inject(forwardRef(() => UserEntityService)) + private userEntityService: UserEntityService, + + private utilityService: UtilityService, + private driveFolderEntityService: DriveFolderEntityService, + ) { + } + + public validateFileName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) && + (name.indexOf('\\') === -1) && + (name.indexOf('/') === -1) && + (name.indexOf('..') === -1) + ); + } + + public getPublicProperties(file: DriveFile): DriveFile['properties'] { + if (file.properties.orientation != null) { + // TODO + //const properties = structuredClone(file.properties); + const properties = JSON.parse(JSON.stringify(file.properties)); + if (file.properties.orientation >= 5) { + [properties.width, properties.height] = [properties.height, properties.width]; + } + properties.orientation = undefined; + return properties; + } + + return file.properties; + } + + public getPublicUrl(file: DriveFile, thumbnail = false): string | null { + // リモートかつメディアプロキシ + if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) { + return appendQuery(this.config.mediaProxy, query({ + url: file.uri, + thumbnail: thumbnail ? '1' : undefined, + })); + } + + // リモートかつ期限切れはローカルプロキシを試みる + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey; + + if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 + return `${this.config.url}/files/${key}`; + } + } + + const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type); + + return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); + } + + public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise { + const id = typeof user === 'object' ? user.id : user; + + const { sum } = await this.driveFilesRepository + .createQueryBuilder('file') + .where('file.userId = :id', { id: id }) + .andWhere('file.isLink = FALSE') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) ?? 0; + } + + public async calcDriveUsageOfHost(host: string): Promise { + const { sum } = await this.driveFilesRepository + .createQueryBuilder('file') + .where('file.userHost = :host', { host: this.utilityService.toPuny(host) }) + .andWhere('file.isLink = FALSE') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) ?? 0; + } + + public async calcDriveUsageOfLocal(): Promise { + const { sum } = await this.driveFilesRepository + .createQueryBuilder('file') + .where('file.userHost IS NULL') + .andWhere('file.isLink = FALSE') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) ?? 0; + } + + public async calcDriveUsageOfRemote(): Promise { + const { sum } = await this.driveFilesRepository + .createQueryBuilder('file') + .where('file.userHost IS NOT NULL') + .andWhere('file.isLink = FALSE') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) ?? 0; + } + + public async pack( + src: DriveFile['id'] | DriveFile, + options?: PackOptions, + ): Promise> { + const opts = Object.assign({ + detail: false, + self: false, + }, options); + + const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneByOrFail({ id: src }); + + return await awaitAll>({ + id: file.id, + createdAt: file.createdAt.toISOString(), + name: file.name, + type: file.type, + md5: file.md5, + size: file.size, + isSensitive: file.isSensitive, + blurhash: file.blurhash, + properties: opts.self ? file.properties : this.getPublicProperties(file), + url: opts.self ? file.url : this.getPublicUrl(file, false), + thumbnailUrl: this.getPublicUrl(file, true), + comment: file.comment, + folderId: file.folderId, + folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { + detail: true, + }) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, + }); + } + + public async packNullable( + src: DriveFile['id'] | DriveFile, + options?: PackOptions, + ): Promise | null> { + const opts = Object.assign({ + detail: false, + self: false, + }, options); + + const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneBy({ id: src }); + if (file == null) return null; + + return await awaitAll>({ + id: file.id, + createdAt: file.createdAt.toISOString(), + name: file.name, + type: file.type, + md5: file.md5, + size: file.size, + isSensitive: file.isSensitive, + blurhash: file.blurhash, + properties: opts.self ? file.properties : this.getPublicProperties(file), + url: opts.self ? file.url : this.getPublicUrl(file, false), + thumbnailUrl: this.getPublicUrl(file, true), + comment: file.comment, + folderId: file.folderId, + folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { + detail: true, + }) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, + }); + } + + public async packMany( + files: (DriveFile['id'] | DriveFile)[], + options?: PackOptions, + ): Promise[]> { + const items = await Promise.all(files.map(f => this.packNullable(f, options))); + return items.filter((x): x is Packed<'DriveFile'> => x != null); + } +} diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts new file mode 100644 index 0000000000..beebbc3206 --- /dev/null +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -0,0 +1,57 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class DriveFolderEntityService { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + ) { + } + + public async pack( + src: DriveFolder['id'] | DriveFolder, + options?: { + detail: boolean + }, + ): Promise> { + const opts = Object.assign({ + detail: false, + }, options); + + const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: folder.id, + createdAt: folder.createdAt.toISOString(), + name: folder.name, + parentId: folder.parentId, + + ...(opts.detail ? { + foldersCount: this.driveFoldersRepository.countBy({ + parentId: folder.id, + }), + filesCount: this.driveFilesRepository.countBy({ + folderId: folder.id, + }), + + ...(folder.parentId ? { + parent: this.pack(folder.parentId, { + detail: true, + }), + } : {}), + } : {}), + }); + } +} + diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts new file mode 100644 index 0000000000..10ed0f19ee --- /dev/null +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -0,0 +1,43 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { EmojisRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class EmojiEntityService { + constructor( + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Emoji['id'] | Emoji, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + return { + id: emoji.id, + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + host: emoji.host, + // ?? emoji.originalUrl してるのは後方互換性のため + url: emoji.publicUrl ?? emoji.originalUrl, + }; + } + + public packMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts new file mode 100644 index 0000000000..f7e7fd42e4 --- /dev/null +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -0,0 +1,34 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { FollowRequestsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { FollowRequest } from '@/models/entities/FollowRequest.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class FollowRequestEntityService { + constructor( + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: FollowRequest['id'] | FollowRequest, + me?: { id: User['id'] } | null | undefined, + ) { + const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); + + return { + id: request.id, + follower: await this.userEntityService.pack(request.followerId, me), + followee: await this.userEntityService.pack(request.followeeId, me), + }; + } +} + diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts new file mode 100644 index 0000000000..93fed85f72 --- /dev/null +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -0,0 +1,98 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { FollowingsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Following } from '@/models/entities/Following.js'; +import { UserEntityService } from './UserEntityService.js'; + +type LocalFollowerFollowing = Following & { + followerHost: null; + followerInbox: null; + followerSharedInbox: null; +}; + +type RemoteFollowerFollowing = Following & { + followerHost: string; + followerInbox: string; + followerSharedInbox: string; +}; + +type LocalFolloweeFollowing = Following & { + followeeHost: null; + followeeInbox: null; + followeeSharedInbox: null; +}; + +type RemoteFolloweeFollowing = Following & { + followeeHost: string; + followeeInbox: string; + followeeSharedInbox: string; +}; + +@Injectable() +export class FollowingEntityService { + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public isLocalFollower(following: Following): following is LocalFollowerFollowing { + return following.followerHost == null; + } + + public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { + return following.followerHost != null; + } + + public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { + return following.followeeHost == null; + } + + public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { + return following.followeeHost != null; + } + + public async pack( + src: Following['id'] | Following, + me?: { id: User['id'] } | null | undefined, + opts?: { + populateFollowee?: boolean; + populateFollower?: boolean; + }, + ): Promise> { + const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src }); + + if (opts == null) opts = {}; + + return await awaitAll({ + id: following.id, + createdAt: following.createdAt.toISOString(), + followeeId: following.followeeId, + followerId: following.followerId, + followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, { + detail: true, + }) : undefined, + follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, { + detail: true, + }) : undefined, + }); + } + + public packMany( + followings: any[], + me?: { id: User['id'] } | null | undefined, + opts?: { + populateFollowee?: boolean; + populateFollower?: boolean; + }, + ) { + return Promise.all(followings.map(x => this.pack(x, me, opts))); + } +} + diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts new file mode 100644 index 0000000000..9473ed90b7 --- /dev/null +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { GalleryPosts, GalleryLikesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { GalleryLike } from '@/models/entities/GalleryLike.js'; +import { UserEntityService } from './UserEntityService.js'; +import { GalleryPostEntityService } from './GalleryPostEntityService.js'; + +@Injectable() +export class GalleryLikeEntityService { + constructor( + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, + + private galleryPostEntityService: GalleryPostEntityService, + ) { + } + + public async pack( + src: GalleryLike['id'] | GalleryLike, + me?: any, + ) { + const like = typeof src === 'object' ? src : await this.galleryLikesRepository.findOneByOrFail({ id: src }); + + return { + id: like.id, + post: await this.galleryPostEntityService.pack(like.post ?? like.postId, me), + }; + } + + public packMany( + likes: any[], + me: any, + ) { + return Promise.all(likes.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts new file mode 100644 index 0000000000..82b41697c9 --- /dev/null +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -0,0 +1,57 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; + +@Injectable() +export class GalleryPostEntityService { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, + + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + ) { + } + + public async pack( + src: GalleryPost['id'] | GalleryPost, + me?: { id: User['id'] } | null | undefined, + ): Promise> { + const meId = me ? me.id : null; + const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: post.id, + createdAt: post.createdAt.toISOString(), + updatedAt: post.updatedAt.toISOString(), + userId: post.userId, + user: this.userEntityService.pack(post.user ?? post.userId, me), + title: post.title, + description: post.description, + fileIds: post.fileIds, + files: this.driveFileEntityService.packMany(post.fileIds), + tags: post.tags.length > 0 ? post.tags : undefined, + isSensitive: post.isSensitive, + likedCount: post.likedCount, + isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, + }); + } + + public packMany( + posts: GalleryPost[], + me?: { id: User['id'] } | null | undefined, + ) { + return Promise.all(posts.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts new file mode 100644 index 0000000000..6dcbf49030 --- /dev/null +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { HashtagsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Hashtag } from '@/models/entities/Hashtag.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class HashtagEntityService { + constructor( + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Hashtag, + ): Promise> { + return { + tag: src.name, + mentionedUsersCount: src.mentionedUsersCount, + mentionedLocalUsersCount: src.mentionedLocalUsersCount, + mentionedRemoteUsersCount: src.mentionedRemoteUsersCount, + attachedUsersCount: src.attachedUsersCount, + attachedLocalUsersCount: src.attachedLocalUsersCount, + attachedRemoteUsersCount: src.attachedRemoteUsersCount, + }; + } + + public packMany( + hashtags: Hashtag[], + ) { + return Promise.all(hashtags.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts new file mode 100644 index 0000000000..c58c2f8f34 --- /dev/null +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -0,0 +1,59 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { InstancesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import { MetaService } from '../MetaService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class InstanceEntityService { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private metaService: MetaService, + ) { + } + + public async pack( + instance: Instance, + ): Promise> { + const meta = await this.metaService.fetch(); + return { + id: instance.id, + caughtAt: instance.caughtAt.toISOString(), + host: instance.host, + usersCount: instance.usersCount, + notesCount: instance.notesCount, + followingCount: instance.followingCount, + followersCount: instance.followersCount, + latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null, + lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), + isNotResponding: instance.isNotResponding, + isSuspended: instance.isSuspended, + isBlocked: meta.blockedHosts.includes(instance.host), + softwareName: instance.softwareName, + softwareVersion: instance.softwareVersion, + openRegistrations: instance.openRegistrations, + name: instance.name, + description: instance.description, + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, + iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, + infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, + }; + } + + public packMany( + instances: Instance[], + ) { + return Promise.all(instances.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/MessagingMessageEntityService.ts b/packages/backend/src/core/entities/MessagingMessageEntityService.ts new file mode 100644 index 0000000000..04467b94e4 --- /dev/null +++ b/packages/backend/src/core/entities/MessagingMessageEntityService.ts @@ -0,0 +1,57 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { MessagingMessagesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { UserGroupEntityService } from './UserGroupEntityService.js'; + +@Injectable() +export class MessagingMessageEntityService { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + private userEntityService: UserEntityService, + private userGroupEntityService: UserGroupEntityService, + private driveFileEntityService: DriveFileEntityService, + ) { + } + + public async pack( + src: MessagingMessage['id'] | MessagingMessage, + me?: { id: User['id'] } | null | undefined, + options?: { + populateRecipient?: boolean, + populateGroup?: boolean, + }, + ): Promise> { + const opts = options ?? { + populateRecipient: true, + populateGroup: true, + }; + + const message = typeof src === 'object' ? src : await this.messagingMessagesRepository.findOneByOrFail({ id: src }); + + return { + id: message.id, + createdAt: message.createdAt.toISOString(), + text: message.text, + userId: message.userId, + user: await this.userEntityService.pack(message.user ?? message.userId, me), + recipientId: message.recipientId, + recipient: message.recipientId && opts.populateRecipient ? await this.userEntityService.pack(message.recipient ?? message.recipientId, me) : undefined, + groupId: message.groupId, + group: message.groupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.group ?? message.groupId) : undefined, + fileId: message.fileId, + file: message.fileId ? await this.driveFileEntityService.pack(message.fileId) : null, + isRead: message.isRead, + reads: message.reads, + }; + } +} + diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts new file mode 100644 index 0000000000..45d15088fc --- /dev/null +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -0,0 +1,44 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { ModerationLog } from '@/models/entities/ModerationLog.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ModerationLogEntityService { + constructor( + @Inject(DI.moderationLogsRepository) + private moderationLogsRepository: ModerationLogsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: ModerationLog['id'] | ModerationLog, + ) { + const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: log.id, + createdAt: log.createdAt.toISOString(), + type: log.type, + info: log.info, + userId: log.userId, + user: this.userEntityService.pack(log.user ?? log.userId, null, { + detail: true, + }), + }); + } + + public packMany( + reports: any[], + ) { + return Promise.all(reports.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts new file mode 100644 index 0000000000..c5245bf203 --- /dev/null +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { MutingsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Muting } from '@/models/entities/Muting.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class MutingEntityService { + constructor( + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Muting['id'] | Muting, + me?: { id: User['id'] } | null | undefined, + ): Promise> { + const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: muting.id, + createdAt: muting.createdAt.toISOString(), + expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, + muteeId: muting.muteeId, + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, + }), + }); + } + + public packMany( + mutings: any[], + me: { id: User['id'] }, + ) { + return Promise.all(mutings.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts new file mode 100644 index 0000000000..eab233bbef --- /dev/null +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -0,0 +1,396 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import * as mfm from 'mfm-js'; +import { ModuleRef } from '@nestjs/core'; +import { DI } from '@/di-symbols.js'; +import type { Notes, Polls, PollVotes, DriveFiles, Channels, Followings, Users, NoteReactions } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { Packed } from '@/misc/schema.js'; +import { nyaize } from '@/misc/nyaize.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { CustomEmojiService } from '../CustomEmojiService.js'; +import type { ReactionService } from '../ReactionService.js'; +import type { UserEntityService } from './UserEntityService.js'; +import type { DriveFileEntityService } from './DriveFileEntityService.js'; + +@Injectable() +export class NoteEntityService implements OnModuleInit { + private userEntityService: UserEntityService; + private driveFileEntityService: DriveFileEntityService; + private customEmojiService: CustomEmojiService; + private reactionService: ReactionService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + //private userEntityService: UserEntityService, + //private driveFileEntityService: DriveFileEntityService, + //private customEmojiService: CustomEmojiService, + //private reactionService: ReactionService, + ) { + } + + onModuleInit() { + this.userEntityService = this.moduleRef.get('UserEntityService'); + this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); + this.customEmojiService = this.moduleRef.get('CustomEmojiService'); + this.reactionService = this.moduleRef.get('ReactionService'); + } + + async #hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { + // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) + let hide = false; + + // visibility が specified かつ自分が指定されていなかったら非表示 + if (packedNote.visibility === 'specified') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else { + // 指定されているかどうか + const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); + + if (specified) { + hide = false; + } else { + hide = true; + } + } + } + + // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 + if (packedNote.visibility === 'followers') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else if (packedNote.reply && (meId === packedNote.reply.userId)) { + // 自分の投稿に対するリプライ + hide = false; + } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { + // 自分へのメンション + hide = false; + } else { + // フォロワーかどうか + const following = await this.followingsRepository.findOneBy({ + followeeId: packedNote.userId, + followerId: meId, + }); + + if (following == null) { + hide = true; + } else { + hide = false; + } + } + } + + if (hide) { + packedNote.visibleUserIds = undefined; + packedNote.fileIds = []; + packedNote.files = []; + packedNote.text = null; + packedNote.poll = undefined; + packedNote.cw = null; + packedNote.isHidden = true; + } + } + + async #populatePoll(note: Note, meId: User['id'] | null) { + const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); + const choices = poll.choices.map(c => ({ + text: c, + votes: poll.votes[poll.choices.indexOf(c)], + isVoted: false, + })); + + if (meId) { + if (poll.multiple) { + const votes = await this.pollVotesRepository.findBy({ + userId: meId, + noteId: note.id, + }); + + const myChoices = votes.map(v => v.choice); + for (const myChoice of myChoices) { + choices[myChoice].isVoted = true; + } + } else { + const vote = await this.pollVotesRepository.findOneBy({ + userId: meId, + noteId: note.id, + }); + + if (vote) { + choices[vote.choice].isVoted = true; + } + } + } + + return { + multiple: poll.multiple, + expiresAt: poll.expiresAt, + choices, + }; + } + + async #populateMyReaction(note: Note, meId: User['id'], _hint_?: { + myReactions: Map; + }) { + if (_hint_?.myReactions) { + const reaction = _hint_.myReactions.get(note.id); + if (reaction) { + return this.reactionService.convertLegacyReaction(reaction.reaction); + } else if (reaction === null) { + return undefined; + } + // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない + } + + const reaction = await this.noteReactionsRepository.findOneBy({ + userId: meId, + noteId: note.id, + }); + + if (reaction) { + return this.reactionService.convertLegacyReaction(reaction.reaction); + } + + return undefined; + } + + public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { + // This code must always be synchronized with the checks in generateVisibilityQuery. + // visibility が specified かつ自分が指定されていなかったら非表示 + if (note.visibility === 'specified') { + if (meId == null) { + return false; + } else if (meId === note.userId) { + return true; + } else { + // 指定されているかどうか + return note.visibleUserIds.some((id: any) => meId === id); + } + } + + // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 + if (note.visibility === 'followers') { + if (meId == null) { + return false; + } else if (meId === note.userId) { + return true; + } else if (note.reply && (meId === note.reply.userId)) { + // 自分の投稿に対するリプライ + return true; + } else if (note.mentions && note.mentions.some(id => meId === id)) { + // 自分へのメンション + return true; + } else { + // フォロワーかどうか + const [following, user] = await Promise.all([ + this.followingsRepository.count({ + where: { + followeeId: note.userId, + followerId: meId, + }, + take: 1, + }), + this.usersRepository.findOneByOrFail({ id: meId }), + ]); + + /* If we know the following, everyhting is fine. + + But if we do not know the following, it might be that both the + author of the note and the author of the like are remote users, + in which case we can never know the following. Instead we have + to assume that the users are following each other. + */ + return following > 0 || (note.userHost != null && user.host != null); + } + } + + return true; + } + + public async pack( + src: Note['id'] | Note, + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + _hint_?: { + myReactions: Map; + }; + }, + ): Promise> { + const opts = Object.assign({ + detail: true, + skipHide: false, + }, options); + + const meId = me ? me.id : null; + const note = typeof src === 'object' ? src : await this.notesRepository.findOneByOrFail({ id: src }); + const host = note.userHost; + + let text = note.text; + + if (note.name && (note.url ?? note.uri)) { + text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url ?? note.uri}`; + } + + const channel = note.channelId + ? note.channel + ? note.channel + : await this.channelsRepository.findOneBy({ id: note.channelId }) + : null; + + const reactionEmojiNames = Object.keys(note.reactions).filter(x => x.startsWith(':')).map(x => this.reactionService.decodeReaction(x).reaction).map(x => x.replace(/:/g, '')); + + const packed: Packed<'Note'> = await awaitAll({ + id: note.id, + createdAt: note.createdAt.toISOString(), + userId: note.userId, + user: this.userEntityService.pack(note.user ?? note.userId, me, { + detail: false, + }), + text: text, + cw: note.cw, + visibility: note.visibility, + localOnly: note.localOnly ?? undefined, + visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, + renoteCount: note.renoteCount, + repliesCount: note.repliesCount, + reactions: this.reactionService.convertLegacyReactions(note.reactions), + tags: note.tags.length > 0 ? note.tags : undefined, + emojis: this.customEmojiService.populateEmojis(note.emojis.concat(reactionEmojiNames), host), + fileIds: note.fileIds, + files: this.driveFileEntityService.packMany(note.fileIds), + replyId: note.replyId, + renoteId: note.renoteId, + channelId: note.channelId ?? undefined, + channel: channel ? { + id: channel.id, + name: channel.name, + } : undefined, + mentions: note.mentions.length > 0 ? note.mentions : undefined, + uri: note.uri ?? undefined, + url: note.url ?? undefined, + + ...(opts.detail ? { + reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { + detail: false, + _hint_: options?._hint_, + }) : undefined, + + renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { + detail: true, + _hint_: options?._hint_, + }) : undefined, + + poll: note.hasPoll ? this.#populatePoll(note, meId) : undefined, + + ...(meId ? { + myReaction: this.#populateMyReaction(note, meId, options?._hint_), + } : {}), + } : {}), + }); + + if (packed.user.isCat && packed.text) { + const tokens = packed.text ? mfm.parse(packed.text) : []; + mfm.inspect(tokens, node => { + if (node.type === 'text') { + // TODO: quoteなtextはskip + node.props.text = nyaize(node.props.text); + } + }); + packed.text = mfm.toString(tokens); + } + + if (!opts.skipHide) { + await this.#hideNote(packed, meId); + } + + return packed; + } + + public async packMany( + notes: Note[], + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + }, + ) { + if (notes.length === 0) return []; + + const meId = me ? me.id : null; + const myReactionsMap = new Map(); + if (meId) { + const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); + const targets = [...notes.map(n => n.id), ...renoteIds]; + const myReactions = await this.noteReactionsRepository.findBy({ + userId: meId, + noteId: In(targets), + }); + + for (const target of targets) { + myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); + } + } + + await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); + + return await Promise.all(notes.map(n => this.pack(n, me, { + ...options, + _hint_: { + myReactions: myReactionsMap, + }, + }))); + } + + public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { + // 指定したユーザーの指定したノートのリノートがいくつあるか数える + const query = this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId }) + .andWhere('note.renoteId = :renoteId', { renoteId }); + + // 指定した投稿を除く + if (excludeNoteId) { + query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); + } + + return await query.getCount(); + } +} diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts new file mode 100644 index 0000000000..f0bbf27b6a --- /dev/null +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { NoteFavoritesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { NoteFavorite } from '@/models/entities/NoteFavorite.js'; +import { UserEntityService } from './UserEntityService.js'; +import { NoteEntityService } from './NoteEntityService.js'; + +@Injectable() +export class NoteFavoriteEntityService { + constructor( + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + private noteEntityService: NoteEntityService, + ) { + } + + public async pack( + src: NoteFavorite['id'] | NoteFavorite, + me?: { id: User['id'] } | null | undefined, + ) { + const favorite = typeof src === 'object' ? src : await this.noteFavoritesRepository.findOneByOrFail({ id: src }); + + return { + id: favorite.id, + createdAt: favorite.createdAt.toISOString(), + noteId: favorite.noteId, + note: await this.noteEntityService.pack(favorite.note ?? favorite.noteId, me), + }; + } + + public packMany( + favorites: any[], + me: { id: User['id'] }, + ) { + return Promise.all(favorites.map(x => this.pack(x, me))); + } +} diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts new file mode 100644 index 0000000000..e64f2af681 --- /dev/null +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { NoteReactionsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { ReactionService } from '../ReactionService.js'; +import type { UserEntityService } from './UserEntityService.js'; +import type { NoteEntityService } from './NoteEntityService.js'; +import { ModuleRef } from '@nestjs/core'; + +@Injectable() +export class NoteReactionEntityService implements OnModuleInit { + private userEntityService: UserEntityService; + private noteEntityService: NoteEntityService; + private reactionService: ReactionService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + //private userEntityService: UserEntityService, + //private noteEntityService: NoteEntityService, + //private reactionService: ReactionService, + ) { + } + + onModuleInit() { + this.userEntityService = this.moduleRef.get('UserEntityService'); + this.noteEntityService = this.moduleRef.get('NoteEntityService'); + this.reactionService = this.moduleRef.get('ReactionService'); + } + + public async pack( + src: NoteReaction['id'] | NoteReaction, + me?: { id: User['id'] } | null | undefined, + options?: { + withNote: boolean; + }, + ): Promise> { + const opts = Object.assign({ + withNote: false, + }, options); + + const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src }); + + return { + id: reaction.id, + createdAt: reaction.createdAt.toISOString(), + user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me), + type: this.reactionService.convertLegacyReaction(reaction.reaction), + ...(opts.withNote ? { + note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), + } : {}), + }; + } +} diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts new file mode 100644 index 0000000000..6a0683d543 --- /dev/null +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -0,0 +1,151 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { ModuleRef } from '@nestjs/core'; +import { DI } from '@/di-symbols.js'; +import { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Notification } from '@/models/entities/Notification.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { Packed } from '@/misc/schema.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { CustomEmojiService } from '../CustomEmojiService.js'; +import type { UserEntityService } from './UserEntityService.js'; +import type { NoteEntityService } from './NoteEntityService.js'; +import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js'; + +@Injectable() +export class NotificationEntityService implements OnModuleInit { + private userEntityService: UserEntityService; + private noteEntityService: NoteEntityService; + private userGroupInvitationEntityService: UserGroupInvitationEntityService; + private customEmojiService: CustomEmojiService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + //private userEntityService: UserEntityService, + //private noteEntityService: NoteEntityService, + //private userGroupInvitationEntityService: UserGroupInvitationEntityService, + //private customEmojiService: CustomEmojiService, + ) { + } + + onModuleInit() { + this.userEntityService = this.moduleRef.get('UserEntityService'); + this.noteEntityService = this.moduleRef.get('NoteEntityService'); + this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService'); + this.customEmojiService = this.moduleRef.get('CustomEmojiService'); + } + + public async pack( + src: Notification['id'] | Notification, + options: { + _hintForEachNotes_?: { + myReactions: Map; + }; + }, + ): Promise> { + const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src }); + const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null; + + return await awaitAll({ + id: notification.id, + createdAt: notification.createdAt.toISOString(), + type: notification.type, + isRead: notification.isRead, + userId: notification.notifierId, + user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null, + ...(notification.type === 'mention' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), + ...(notification.type === 'reply' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), + ...(notification.type === 'renote' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), + ...(notification.type === 'quote' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), + ...(notification.type === 'reaction' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + reaction: notification.reaction, + } : {}), + ...(notification.type === 'pollVote' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + choice: notification.choice, + } : {}), + ...(notification.type === 'pollEnded' ? { + note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), + ...(notification.type === 'groupInvited' ? { + invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!), + } : {}), + ...(notification.type === 'app' ? { + body: notification.customBody, + header: notification.customHeader ?? token?.name, + icon: notification.customIcon ?? token?.iconUrl, + } : {}), + }); + } + + public async packMany( + notifications: Notification[], + meId: User['id'], + ) { + if (notifications.length === 0) return []; + + const notes = notifications.filter(x => x.note != null).map(x => x.note!); + const noteIds = notes.map(n => n.id); + const myReactionsMap = new Map(); + const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); + const targets = [...noteIds, ...renoteIds]; + const myReactions = await this.noteReactionsRepository.findBy({ + userId: meId, + noteId: In(targets), + }); + + for (const target of targets) { + myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); + } + + await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); + + return await Promise.all(notifications.map(x => this.pack(x, { + _hintForEachNotes_: { + myReactions: myReactionsMap, + }, + }))); + } +} diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts new file mode 100644 index 0000000000..cbd193fe0a --- /dev/null +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Page } from '@/models/entities/Page.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; + +@Injectable() +export class PageEntityService { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + ) { + } + + public async pack( + src: Page['id'] | Page, + me?: { id: User['id'] } | null | undefined, + ): Promise> { + const meId = me ? me.id : null; + const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src }); + + const attachedFiles: Promise[] = []; + const collectFile = (xs: any[]) => { + for (const x of xs) { + if (x.type === 'image') { + attachedFiles.push(this.driveFilesRepository.findOneBy({ + id: x.fileId, + userId: page.userId, + })); + } + if (x.children) { + collectFile(x.children); + } + } + }; + collectFile(page.content); + + // 後方互換性のため + let migrated = false; + const migrate = (xs: any[]) => { + for (const x of xs) { + if (x.type === 'input') { + if (x.inputType === 'text') { + x.type = 'textInput'; + } + if (x.inputType === 'number') { + x.type = 'numberInput'; + if (x.default) x.default = parseInt(x.default, 10); + } + migrated = true; + } + if (x.children) { + migrate(x.children); + } + } + }; + migrate(page.content); + if (migrated) { + this.pagesRepository.update(page.id, { + content: page.content, + }); + } + + return await awaitAll({ + id: page.id, + createdAt: page.createdAt.toISOString(), + updatedAt: page.updatedAt.toISOString(), + userId: page.userId, + user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意 + content: page.content, + variables: page.variables, + title: page.title, + name: page.name, + summary: page.summary, + hideTitleWhenPinned: page.hideTitleWhenPinned, + alignCenter: page.alignCenter, + font: page.font, + script: page.script, + eyeCatchingImageId: page.eyeCatchingImageId, + eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), + likedCount: page.likedCount, + isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, + }); + } + + public packMany( + pages: Page[], + me?: { id: User['id'] } | null | undefined, + ) { + return Promise.all(pages.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts new file mode 100644 index 0000000000..dccaf2edec --- /dev/null +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { PageLikesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { PageLike } from '@/models/entities/PageLike.js'; +import { UserEntityService } from './UserEntityService.js'; +import { PageEntityService } from './PageEntityService.js'; + +@Injectable() +export class PageLikeEntityService { + constructor( + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, + + private pageEntityService: PageEntityService, + ) { + } + + public async pack( + src: PageLike['id'] | PageLike, + me?: { id: User['id'] } | null | undefined, + ) { + const like = typeof src === 'object' ? src : await this.pageLikesRepository.findOneByOrFail({ id: src }); + + return { + id: like.id, + page: await this.pageEntityService.pack(like.page ?? like.pageId, me), + }; + } + + public packMany( + likes: any[], + me: { id: User['id'] }, + ) { + return Promise.all(likes.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts new file mode 100644 index 0000000000..521c7669e7 --- /dev/null +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { SigninsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Signin } from '@/models/entities/Signin.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class SigninEntityService { + constructor( + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: Signin, + ) { + return src; + } +} + diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts new file mode 100644 index 0000000000..48d8af83fb --- /dev/null +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -0,0 +1,515 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { EntityRepository, Repository, In, Not } from 'typeorm'; +import Ajv from 'ajv'; +import { ModuleRef } from '@nestjs/core'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Promiseable } from '@/misc/prelude/await-all.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; +import { Cache } from '@/misc/cache.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { AntennaService } from '../AntennaService.js'; +import type { CustomEmojiService } from '../CustomEmojiService.js'; +import type { NoteEntityService } from './NoteEntityService.js'; +import type { DriveFileEntityService } from './DriveFileEntityService.js'; +import type { PageEntityService } from './PageEntityService.js'; + +type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; +type IsMeAndIsUserDetailed = + Detailed extends true ? + ExpectsMe extends true ? Packed<'MeDetailed'> : + ExpectsMe extends false ? Packed<'UserDetailedNotMe'> : + Packed<'UserDetailed'> : + Packed<'UserLite'>; + +const ajv = new Ajv(); + +function isLocalUser(user: User): user is ILocalUser; +function isLocalUser(user: T): user is T & { host: null; }; +function isLocalUser(user: User | { host: User['host'] }): boolean { + return user.host == null; +} + +function isRemoteUser(user: User): user is IRemoteUser; +function isRemoteUser(user: T): user is T & { host: string; }; +function isRemoteUser(user: User | { host: User['host'] }): boolean { + return !isLocalUser(user); +} + +@Injectable() +export class UserEntityService implements OnModuleInit { + private noteEntityService: NoteEntityService; + private driveFileEntityService: DriveFileEntityService; + private pageEntityService: PageEntityService; + private customEmojiService: CustomEmojiService; + private antennaService: AntennaService; + #userInstanceCache: Cache; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + //private noteEntityService: NoteEntityService, + //private driveFileEntityService: DriveFileEntityService, + //private pageEntityService: PageEntityService, + //private customEmojiService: CustomEmojiService, + //private antennaService: AntennaService, + ) { + this.#userInstanceCache = new Cache(1000 * 60 * 60 * 3); + } + + onModuleInit() { + this.noteEntityService = this.moduleRef.get('NoteEntityService'); + this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); + this.pageEntityService = this.moduleRef.get('PageEntityService'); + this.customEmojiService = this.moduleRef.get('CustomEmojiService'); + this.antennaService = this.moduleRef.get('AntennaService'); + } + + //#region Validators + public validateLocalUsername = ajv.compile(localUsernameSchema); + public validatePassword = ajv.compile(passwordSchema); + public validateName = ajv.compile(nameSchema); + public validateDescription = ajv.compile(descriptionSchema); + public validateLocation = ajv.compile(locationSchema); + public validateBirthday = ajv.compile(birthdaySchema); + //#endregion + + public isLocalUser = isLocalUser; + public isRemoteUser = isRemoteUser; + + public async getRelation(me: User['id'], target: User['id']) { + return awaitAll({ + id: target, + isFollowing: this.followingsRepository.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + isFollowed: this.followingsRepository.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestFromYou: this.followRequestsRepository.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestToYou: this.followRequestsRepository.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + isBlocking: this.blockingsRepository.count({ + where: { + blockerId: me, + blockeeId: target, + }, + take: 1, + }).then(n => n > 0), + isBlocked: this.blockingsRepository.count({ + where: { + blockerId: target, + blockeeId: me, + }, + take: 1, + }).then(n => n > 0), + isMuted: this.mutingsRepository.count({ + where: { + muterId: me, + muteeId: target, + }, + take: 1, + }).then(n => n > 0), + }); + } + + public async getHasUnreadMessagingMessage(userId: User['id']): Promise { + const mute = await this.mutingsRepository.findBy({ + muterId: userId, + }); + + const joinings = await this.userGroupJoiningsRepository.findBy({ userId: userId }); + + const groupQs = Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder('message') + .where('message.groupId = :groupId', { groupId: j.userGroupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null))); + + const [withUser, withGroups] = await Promise.all([ + this.messagingMessagesRepository.count({ + where: { + recipientId: userId, + isRead: false, + ...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}), + }, + take: 1, + }).then(count => count > 0), + groupQs, + ]); + + return withUser || withGroups.some(x => x); + } + + public async getHasUnreadAnnouncement(userId: User['id']): Promise { + const reads = await this.announcementReadsRepository.findBy({ + userId: userId, + }); + + const count = await this.announcementsRepository.countBy(reads.length > 0 ? { + id: Not(In(reads.map(read => read.announcementId))), + } : {}); + + return count > 0; + } + + public async getHasUnreadAntenna(userId: User['id']): Promise { + const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); + + const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({ + antennaId: In(myAntennas.map(x => x.id)), + read: false, + }) : null; + + return unread != null; + } + + public async getHasUnreadChannel(userId: User['id']): Promise { + const channels = await this.channelFollowingsRepository.findBy({ followerId: userId }); + + const unread = channels.length > 0 ? await this.noteUnreadsRepository.findOneBy({ + userId: userId, + noteChannelId: In(channels.map(x => x.followeeId)), + }) : null; + + return unread != null; + } + + public async getHasUnreadNotification(userId: User['id']): Promise { + const mute = await this.mutingsRepository.findBy({ + muterId: userId, + }); + const mutedUserIds = mute.map(m => m.muteeId); + + const count = await this.notificationsRepository.count({ + where: { + notifieeId: userId, + ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), + isRead: false, + }, + take: 1, + }); + + return count > 0; + } + + public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { + const count = await this.followRequestsRepository.countBy({ + followeeId: userId, + }); + + return count > 0; + } + + public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { + if (user.hideOnlineStatus) return 'unknown'; + if (user.lastActiveDate == null) return 'unknown'; + const elapsed = Date.now() - user.lastActiveDate.getTime(); + return ( + elapsed < USER_ONLINE_THRESHOLD ? 'online' : + elapsed < USER_ACTIVE_THRESHOLD ? 'active' : + 'offline' + ); + } + + public async getAvatarUrl(user: User): Promise { + if (user.avatar) { + return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + } else if (user.avatarId) { + const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); + return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id); + } else { + return this.getIdenticonUrl(user.id); + } + } + + public getAvatarUrlSync(user: User): string { + if (user.avatar) { + return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + } else { + return this.getIdenticonUrl(user.id); + } + } + + public getIdenticonUrl(userId: User['id']): string { + return `${this.config.url}/identicon/${userId}`; + } + + public async pack( + src: User['id'] | User, + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: D, + includeSecrets?: boolean, + }, + ): Promise> { + const opts = Object.assign({ + detail: false, + includeSecrets: false, + }, options); + + let user: User; + + if (typeof src === 'object') { + user = src; + if (src.avatar === undefined && src.avatarId) src.avatar = await this.driveFilesRepository.findOneBy({ id: src.avatarId }) ?? null; + if (src.banner === undefined && src.bannerId) src.banner = await this.driveFilesRepository.findOneBy({ id: src.bannerId }) ?? null; + } else { + user = await this.usersRepository.findOneOrFail({ + where: { id: src }, + relations: { + avatar: true, + banner: true, + }, + }); + } + + const meId = me ? me.id : null; + const isMe = meId === user.id; + + const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; + const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') + .where('pin.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('pin.note', 'note') + .orderBy('pin.id', 'DESC') + .getMany() : []; + const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; + + const followingCount = profile == null ? null : + (profile.ffVisibility === 'public') || isMe ? user.followingCount : + (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : + null; + + const followersCount = profile == null ? null : + (profile.ffVisibility === 'public') || isMe ? user.followersCount : + (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : + null; + + const falsy = opts.detail ? false : undefined; + + const packed = { + id: user.id, + name: user.name, + username: user.username, + host: user.host, + avatarUrl: this.getAvatarUrlSync(user), + avatarBlurhash: user.avatar?.blurhash ?? null, + avatarColor: null, // 後方互換性のため + isAdmin: user.isAdmin ?? falsy, + isModerator: user.isModerator ?? falsy, + isBot: user.isBot ?? falsy, + isCat: user.isCat ?? falsy, + instance: user.host ? this.#userInstanceCache.fetch(user.host, + () => this.instancesRepository.findOneBy({ host: user.host! }), + v => v != null, + ).then(instance => instance ? { + name: instance.name, + softwareName: instance.softwareName, + softwareVersion: instance.softwareVersion, + iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, + } : undefined) : undefined, + emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), + onlineStatus: this.getOnlineStatus(user), + driveCapacityOverrideMb: user.driveCapacityOverrideMb, + + ...(opts.detail ? { + url: profile!.url, + uri: user.uri, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, + lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, + bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, + bannerBlurhash: user.banner?.blurhash ?? null, + bannerColor: null, // 後方互換性のため + isLocked: user.isLocked, + isSilenced: user.isSilenced ?? falsy, + isSuspended: user.isSuspended ?? falsy, + description: profile!.description, + location: profile!.location, + birthday: profile!.birthday, + lang: profile!.lang, + fields: profile!.fields, + followersCount: followersCount ?? 0, + followingCount: followingCount ?? 0, + notesCount: user.notesCount, + pinnedNoteIds: pins.map(pin => pin.noteId), + pinnedNotes: this.noteEntityService.packMany(pins.map(pin => pin.note!), me, { + detail: true, + }), + pinnedPageId: profile!.pinnedPageId, + pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, + publicReactions: profile!.publicReactions, + ffVisibility: profile!.ffVisibility, + twoFactorEnabled: profile!.twoFactorEnabled, + usePasswordLessLogin: profile!.usePasswordLessLogin, + securityKeys: profile!.twoFactorEnabled + ? this.userSecurityKeysRepository.countBy({ + userId: user.id, + }).then(result => result >= 1) + : false, + } : {}), + + ...(opts.detail && isMe ? { + avatarId: user.avatarId, + bannerId: user.bannerId, + injectFeaturedNote: profile!.injectFeaturedNote, + receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, + alwaysMarkNsfw: profile!.alwaysMarkNsfw, + autoSensitive: profile!.autoSensitive, + carefulBot: profile!.carefulBot, + autoAcceptFollowed: profile!.autoAcceptFollowed, + noCrawle: profile!.noCrawle, + isExplorable: user.isExplorable, + isDeleted: user.isDeleted, + hideOnlineStatus: user.hideOnlineStatus, + hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ + where: { userId: user.id, isSpecified: true }, + take: 1, + }).then(count => count > 0), + hasUnreadMentions: this.noteUnreadsRepository.count({ + where: { userId: user.id, isMentioned: true }, + take: 1, + }).then(count => count > 0), + hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), + hasUnreadAntenna: this.getHasUnreadAntenna(user.id), + hasUnreadChannel: this.getHasUnreadChannel(user.id), + hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), + hasUnreadNotification: this.getHasUnreadNotification(user.id), + hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), + integrations: profile!.integrations, + mutedWords: profile!.mutedWords, + mutedInstances: profile!.mutedInstances, + mutingNotificationTypes: profile!.mutingNotificationTypes, + emailNotificationTypes: profile!.emailNotificationTypes, + showTimelineReplies: user.showTimelineReplies ?? falsy, + } : {}), + + ...(opts.includeSecrets ? { + email: profile!.email, + emailVerified: profile!.emailVerified, + securityKeysList: profile!.twoFactorEnabled + ? this.userSecurityKeysRepository.find({ + where: { + userId: user.id, + }, + select: { + id: true, + name: true, + lastUsed: true, + }, + }) + : [], + } : {}), + + ...(relation ? { + isFollowing: relation.isFollowing, + isFollowed: relation.isFollowed, + hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou, + isBlocking: relation.isBlocking, + isBlocked: relation.isBlocked, + isMuted: relation.isMuted, + } : {}), + } as Promiseable> as Promiseable>; + + return await awaitAll(packed); + } + + public packMany( + users: (User['id'] | User)[], + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: D, + includeSecrets?: boolean, + }, + ): Promise[]> { + return Promise.all(users.map(u => this.pack(u, me, options))); + } +} diff --git a/packages/backend/src/core/entities/UserGroupEntityService.ts b/packages/backend/src/core/entities/UserGroupEntityService.ts new file mode 100644 index 0000000000..acd26ea1eb --- /dev/null +++ b/packages/backend/src/core/entities/UserGroupEntityService.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class UserGroupEntityService { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: UserGroup['id'] | UserGroup, + ): Promise> { + const userGroup = typeof src === 'object' ? src : await this.userGroupsRepository.findOneByOrFail({ id: src }); + + const users = await this.userGroupJoiningsRepository.findBy({ + userGroupId: userGroup.id, + }); + + return { + id: userGroup.id, + createdAt: userGroup.createdAt.toISOString(), + name: userGroup.name, + ownerId: userGroup.userId, + userIds: users.map(x => x.userId), + }; + } +} + diff --git a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts new file mode 100644 index 0000000000..50ff2231ab --- /dev/null +++ b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts @@ -0,0 +1,39 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { UserGroupInvitationsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; +import { UserEntityService } from './UserEntityService.js'; +import { UserGroupEntityService } from './UserGroupEntityService.js'; + +@Injectable() +export class UserGroupInvitationEntityService { + constructor( + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, + + private userGroupEntityService: UserGroupEntityService, + ) { + } + + public async pack( + src: UserGroupInvitation['id'] | UserGroupInvitation, + ) { + const invitation = typeof src === 'object' ? src : await this.userGroupInvitationsRepository.findOneByOrFail({ id: src }); + + return { + id: invitation.id, + group: await this.userGroupEntityService.pack(invitation.userGroup ?? invitation.userGroupId), + }; + } + + public packMany( + invitations: any[], + ) { + return Promise.all(invitations.map(x => this.pack(x))); + } +} + diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts new file mode 100644 index 0000000000..05434d9c8a --- /dev/null +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class UserListEntityService { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private userEntityService: UserEntityService, + ) { + } + + public async pack( + src: UserList['id'] | UserList, + ): Promise> { + const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); + + const users = await this.userListJoiningsRepository.findBy({ + userListId: userList.id, + }); + + return { + id: userList.id, + createdAt: userList.createdAt.toISOString(), + name: userList.name, + userIds: users.map(x => x.userId), + }; + } +} + diff --git a/packages/backend/src/core/queue/QueueModule.ts b/packages/backend/src/core/queue/QueueModule.ts new file mode 100644 index 0000000000..3a271ea37f --- /dev/null +++ b/packages/backend/src/core/queue/QueueModule.ts @@ -0,0 +1,112 @@ +import { Module } from '@nestjs/common'; +import Bull from 'bull'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import type { Provider } from '@nestjs/common'; +import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../../queue/types.js'; + +function q(config: Config, name: string, limitPerSec = -1) { + return new Bull(name, { + redis: { + port: config.redis.port, + host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + db: config.redis.db ?? 0, + }, + prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', + limiter: limitPerSec > 0 ? { + max: limitPerSec, + duration: 1000, + } : undefined, + settings: { + backoffStrategies: { + apBackoff, + }, + }, + }); +} + +// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 +function apBackoff(attemptsMade: number, err: Error) { + const baseDelay = 60 * 1000; // 1min + const maxBackoff = 8 * 60 * 60 * 1000; // 8hours + let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; + backoff = Math.min(backoff, maxBackoff); + backoff += Math.round(backoff * Math.random() * 0.2); + return backoff; +} + +export type SystemQueue = Bull.Queue>; +export type EndedPollNotificationQueue = Bull.Queue; +export type DeliverQueue = Bull.Queue; +export type InboxQueue = Bull.Queue; +export type DbQueue = Bull.Queue; +export type ObjectStorageQueue = Bull.Queue; +export type WebhookDeliverQueue = Bull.Queue; + +const $system: Provider = { + provide: 'queue:system', + useFactory: (config: Config) => q(config, 'system'), + inject: [DI.config], +}; + +const $endedPollNotification: Provider = { + provide: 'queue:endedPollNotification', + useFactory: (config: Config) => q(config, 'endedPollNotification'), + inject: [DI.config], +}; + +const $deliver: Provider = { + provide: 'queue:deliver', + useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128), + inject: [DI.config], +}; + +const $inbox: Provider = { + provide: 'queue:inbox', + useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16), + inject: [DI.config], +}; + +const $db: Provider = { + provide: 'queue:db', + useFactory: (config: Config) => q(config, 'db'), + inject: [DI.config], +}; + +const $objectStorage: Provider = { + provide: 'queue:objectStorage', + useFactory: (config: Config) => q(config, 'objectStorage'), + inject: [DI.config], +}; + +const $webhookDeliver: Provider = { + provide: 'queue:webhookDeliver', + useFactory: (config: Config) => q(config, 'webhookDeliver', 64), + inject: [DI.config], +}; + +@Module({ + imports: [ + ], + providers: [ + $system, + $endedPollNotification, + $deliver, + $inbox, + $db, + $objectStorage, + $webhookDeliver, + ], + exports: [ + $system, + $endedPollNotification, + $deliver, + $inbox, + $db, + $objectStorage, + $webhookDeliver, + ], +}) +export class QueueModule {} diff --git a/packages/backend/src/core/remote/RemoteLoggerService.ts b/packages/backend/src/core/remote/RemoteLoggerService.ts new file mode 100644 index 0000000000..7ce8fe6cfc --- /dev/null +++ b/packages/backend/src/core/remote/RemoteLoggerService.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Logger from '@/logger.js'; + +@Injectable() +export class RemoteLoggerService { + public logger: Logger; + + constructor( + ) { + this.logger = new Logger('remote', 'cyan'); + } +} diff --git a/packages/backend/src/core/remote/ResolveUserService.ts b/packages/backend/src/core/remote/ResolveUserService.ts new file mode 100644 index 0000000000..4ef91bc8e6 --- /dev/null +++ b/packages/backend/src/core/remote/ResolveUserService.ts @@ -0,0 +1,132 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import chalk from 'chalk'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import type { IRemoteUser, User } from '@/models/entities/User.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { UtilityService } from '../UtilityService.js'; +import { WebfingerService } from './WebfingerService.js'; +import { RemoteLoggerService } from './RemoteLoggerService.js'; +import { ApPersonService } from './activitypub/models/ApPersonService.js'; + +@Injectable() +export class ResolveUserService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private utilityService: UtilityService, + private webfingerService: WebfingerService, + private remoteLoggerService: RemoteLoggerService, + private apPersonService: ApPersonService, + ) { + this.#logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); + } + + public async resolveUser(username: string, host: string | null): Promise { + const usernameLower = username.toLowerCase(); + + if (host == null) { + this.#logger.info(`return local user: ${usernameLower}`); + return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); + } + + host = this.utilityService.toPuny(host); + + if (this.config.host === host) { + this.#logger.info(`return local user: ${usernameLower}`); + return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); + } + + const user = await this.usersRepository.findOneBy({ usernameLower, host }) as IRemoteUser | null; + + const acctLower = `${usernameLower}@${host}`; + + if (user == null) { + const self = await this.#resolveSelf(acctLower); + + this.#logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); + return await this.apPersonService.createPerson(self.href); + } + + // ユーザー情報が古い場合は、WebFilgerからやりなおして返す + if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { + // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する + await this.usersRepository.update(user.id, { + lastFetchedAt: new Date(), + }); + + this.#logger.info(`try resync: ${acctLower}`); + const self = await this.#resolveSelf(acctLower); + + if (user.uri !== self.href) { + // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping. + this.#logger.info(`uri missmatch: ${acctLower}`); + this.#logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); + + // validate uri + const uri = new URL(self.href); + if (uri.hostname !== host) { + throw new Error('Invalid uri'); + } + + await this.usersRepository.update({ + usernameLower, + host: host, + }, { + uri: self.href, + }); + } else { + this.#logger.info(`uri is fine: ${acctLower}`); + } + + await this.apPersonService.updatePerson(self.href); + + this.#logger.info(`return resynced remote user: ${acctLower}`); + return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); + } + + this.#logger.info(`return existing remote user: ${acctLower}`); + return user; + } + + async #resolveSelf(acctLower: string) { + this.#logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); + const finger = await this.webfingerService.webfinger(acctLower).catch(err => { + this.#logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); + throw new Error(`Failed to WebFinger for ${acctLower}: ${ err.statusCode ?? err.message }`); + }); + const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); + if (!self) { + this.#logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); + throw new Error('self link not found'); + } + return self; + } +} diff --git a/packages/backend/src/core/remote/WebfingerService.ts b/packages/backend/src/core/remote/WebfingerService.ts new file mode 100644 index 0000000000..24ccaf528b --- /dev/null +++ b/packages/backend/src/core/remote/WebfingerService.ts @@ -0,0 +1,48 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { query as urlQuery } from '@/misc/prelude/url.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; + +type ILink = { + href: string; + rel?: string; +}; + +type IWebFinger = { + links: ILink[]; + subject: string; +}; + +@Injectable() +export class WebfingerService { + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + } + + public async webfinger(query: string): Promise { + const url = this.#genUrl(query); + + return await this.httpRequestService.getJson(url, 'application/jrd+json, application/json') as IWebFinger; + } + + #genUrl(query: string): string { + if (query.match(/^https?:\/\//)) { + const u = new URL(query); + return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); + } + + const m = query.match(/^([^@]+)@(.*)/); + if (m) { + const hostname = m[2]; + return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` }); + } + + throw new Error(`Invalid query (${query})`); + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApAudienceService.ts b/packages/backend/src/core/remote/activitypub/ApAudienceService.ts new file mode 100644 index 0000000000..178631ac55 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApAudienceService.ts @@ -0,0 +1,104 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +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 { 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'; +import type { Resolver } from './ApResolverService.js'; + +type Visibility = 'public' | 'home' | 'followers' | 'specified'; + +type AudienceInfo = { + visibility: Visibility, + mentionedUsers: CacheableUser[], + visibleUsers: CacheableUser[], +}; + +@Injectable() +export class ApAudienceService { + constructor( + private apPersonService: ApPersonService, + ) { + } + + 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); + + const others = unique(concat([toGroups.other, ccGroups.other])); + + const limit = promiseLimit(2); + const mentionedUsers = (await Promise.all( + others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), + )).filter((x): x is CacheableUser => x != null); + + if (toGroups.public.length > 0) { + return { + visibility: 'public', + mentionedUsers, + visibleUsers: [], + }; + } + + if (ccGroups.public.length > 0) { + return { + visibility: 'home', + mentionedUsers, + visibleUsers: [], + }; + } + + if (toGroups.followers.length > 0) { + return { + visibility: 'followers', + mentionedUsers, + visibleUsers: [], + }; + } + + return { + visibility: 'specified', + mentionedUsers, + visibleUsers: mentionedUsers, + }; + } + + #groupingAudience(ids: string[], actor: CacheableRemoteUser) { + const groups = { + public: [] as string[], + followers: [] as string[], + other: [] as string[], + }; + + for (const id of ids) { + if (this.#isPublic(id)) { + groups.public.push(id); + } else if (this.#isFollowers(id, actor)) { + groups.followers.push(id); + } else { + groups.other.push(id); + } + } + + groups.other = unique(groups.other); + + return groups; + } + + #isPublic(id: string) { + return [ + 'https://www.w3.org/ns/activitystreams#Public', + 'as#Public', + 'Public', + ].includes(id); + } + + #isFollowers(id: string, actor: CacheableRemoteUser) { + return ( + id === (actor.followersUri ?? `${actor.uri}/followers`) + ); + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts b/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts new file mode 100644 index 0000000000..37f58c6b0c --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts @@ -0,0 +1,179 @@ +import { Inject, Injectable } from '@nestjs/common'; +import escapeRegexp from 'escape-regexp'; +import { DI } from '@/di-symbols.js'; +import { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; +import { Cache } from '@/misc/cache.js'; +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 { getApId } from './type.js'; +import { ApPersonService } from './models/ApPersonService.js'; +import type { IObject } from './type.js'; + +export type UriParseResult = { + /** wether the URI was generated by us */ + local: true; + /** id in DB */ + id: string; + /** hint of type, e.g. "notes", "users" */ + type: string; + /** any remaining text after type and id, not including the slash after id. undefined if empty */ + rest?: string; +} | { + /** wether the URI was generated by us */ + local: false; + /** uri in DB */ + uri: string; +}; + +@Injectable() +export class ApDbResolverService { + #publicKeyCache: Cache; + #publicKeyByUserIdCache: Cache; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.userPublickeysRepository) + private userPublickeysRepository: UserPublickeysRepository, + + private userCacheService: UserCacheService, + private apPersonService: ApPersonService, + ) { + this.#publicKeyCache = new Cache(Infinity); + this.#publicKeyByUserIdCache = new Cache(Infinity); + } + + public parseUri(value: string | IObject): UriParseResult { + const uri = getApId(value); + + // the host part of a URL is case insensitive, so use the 'i' flag. + const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + local: true, + type: matchLocal[1], + id: matchLocal[2], + rest: matchLocal[3], + }; + } else { + return { + local: false, + uri, + }; + } + } + + /** + * AP Note => Misskey Note in DB + */ + public async getNoteFromApId(value: string | IObject): Promise { + const parsed = this.parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; + + return await this.notesRepository.findOneBy({ + id: parsed.id, + }); + } else { + return await this.notesRepository.findOneBy({ + uri: parsed.uri, + }); + } + } + + public async getMessageFromApId(value: string | IObject): Promise { + const parsed = this.parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; + + return await this.messagingMessagesRepository.findOneBy({ + id: parsed.id, + }); + } else { + return await this.messagingMessagesRepository.findOneBy({ + uri: parsed.uri, + }); + } + } + + /** + * AP Person => Misskey User in DB + */ + public async getUserFromApId(value: string | IObject): Promise { + const parsed = this.parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'users') return null; + + return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ + id: parsed.id, + }).then(x => x ?? undefined)) ?? null; + } else { + return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ + uri: parsed.uri, + })); + } + } + + /** + * AP KeyId => Misskey User and Key + */ + public async getAuthUserFromKeyId(keyId: string): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey; + } | null> { + const key = await this.#publicKeyCache.fetch(keyId, async () => { + const key = await this.userPublickeysRepository.findOneBy({ + keyId, + }); + + if (key == null) return null; + + return key; + }, key => key != null); + + if (key == null) return null; + + return { + user: await this.userCacheService.userByIdCache.fetch(key.userId, () => this.usersRepository.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser, + key, + }; + } + + /** + * AP Actor id => Misskey User and Key + */ + public async getAuthUserFromApId(uri: string): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey | null; + } | null> { + const user = await this.apPersonService.resolvePerson(uri) as CacheableRemoteUser; + + if (user == null) return null; + + const key = await this.#publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); + + return { + user, + key, + }; + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts new file mode 100644 index 0000000000..a6ee857526 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts @@ -0,0 +1,199 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import { 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'; + +interface IRecipe { + type: string; +} + +interface IFollowersRecipe extends IRecipe { + type: 'Followers'; +} + +interface IDirectRecipe extends IRecipe { + type: 'Direct'; + to: IRemoteUser; +} + +const isFollowers = (recipe: any): recipe is IFollowersRecipe => + recipe.type === 'Followers'; + +const isDirect = (recipe: any): recipe is IDirectRecipe => + recipe.type === 'Direct'; + +@Injectable() +export class ApDeliverManagerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private queueService: QueueService, + ) { + } + + /** + * Deliver activity to followers + * @param activity Activity + * @param from Followee + */ + public async deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addFollowersRecipe(); + await manager.execute(); + } + + /** + * Deliver activity to user + * @param activity Activity + * @param to Target user + */ + public async deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addDirectRecipe(to); + await manager.execute(); + } + + public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { + return new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + + actor, + activity, + ); + } +} + +class DeliverManager { + private actor: { id: User['id']; host: null; }; + private activity: any; + private recipes: IRecipe[] = []; + + /** + * Constructor + * @param actor Actor + * @param activity Activity to deliver + */ + constructor( + private userEntityService: UserEntityService, + private followingsRepository: FollowingsRepository, + private queueService: QueueService, + + actor: { id: User['id']; host: null; }, + activity: any, + ) { + this.actor = actor; + this.activity = activity; + } + + /** + * Add recipe for followers deliver + */ + public addFollowersRecipe() { + const deliver = { + type: 'Followers', + } as IFollowersRecipe; + + this.addRecipe(deliver); + } + + /** + * Add recipe for direct deliver + * @param to To + */ + public addDirectRecipe(to: IRemoteUser) { + const recipe = { + type: 'Direct', + to, + } as IDirectRecipe; + + this.addRecipe(recipe); + } + + /** + * Add recipe + * @param recipe Recipe + */ + public addRecipe(recipe: IRecipe) { + this.recipes.push(recipe); + } + + /** + * Execute delivers + */ + public async execute() { + if (!this.userEntityService.isLocalUser(this.actor)) return; + + const inboxes = new Set(); + + /* + build inbox list + + Process follower recipes first to avoid duplication when processing + direct recipes later. + */ + if (this.recipes.some(r => isFollowers(r))) { + // followers deliver + // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう + // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? + const followers = await this.followingsRepository.find({ + where: { + followeeId: this.actor.id, + followerHost: Not(IsNull()), + }, + select: { + followerSharedInbox: true, + followerInbox: true, + }, + }) as { + followerSharedInbox: string | null; + followerInbox: string; + }[]; + + for (const following of followers) { + const inbox = following.followerSharedInbox ?? following.followerInbox; + inboxes.add(inbox); + } + } + + this.recipes.filter((recipe): recipe is IDirectRecipe => + // followers recipes have already been processed + isDirect(recipe) + // check that shared inbox has not been added yet + && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) + // check that they actually have an inbox + && recipe.to.inbox != null, + ) + .forEach(recipe => inboxes.add(recipe.to.inbox!)); + + // deliver + for (const inbox of inboxes) { + this.queueService.deliver(this.actor, this.activity, inbox); + } + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApInboxService.ts b/packages/backend/src/core/remote/activitypub/ApInboxService.ts new file mode 100644 index 0000000000..531a774c50 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApInboxService.ts @@ -0,0 +1,735 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { ReactionService } from '@/core/ReactionService.js'; +import { RelayService } from '@/core/RelayService.js'; +import { NotePiningService } from '@/core/NotePiningService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; +import { AppLockService } from '@/core/AppLockService.js'; +import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; +import { IdService } from '@/core/IdService.js'; +import { StatusError } from '@/misc/status-error.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { MessagingService } from '@/core/MessagingService.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 { ApNoteService } from './models/ApNoteService.js'; +import { ApLoggerService } from './ApLoggerService.js'; +import { ApDbResolverService } from './ApDbResolverService.js'; +import { ApResolverService } from './ApResolverService.js'; +import { ApAudienceService } from './ApAudienceService.js'; +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'; + +@Injectable() +export class ApInboxService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private utilityService: UtilityService, + private idService: IdService, + private metaService: MetaService, + private userFollowingService: UserFollowingService, + private apAudienceService: ApAudienceService, + private reactionService: ReactionService, + private relayService: RelayService, + private notePiningService: NotePiningService, + private userBlockingService: UserBlockingService, + private noteCreateService: NoteCreateService, + private noteDeleteService: NoteDeleteService, + private appLockService: AppLockService, + private apResolverService: ApResolverService, + private apDbResolverService: ApDbResolverService, + private apLoggerService: ApLoggerService, + private apNoteService: ApNoteService, + private apPersonService: ApPersonService, + private apQuestionService: ApQuestionService, + private queueService: QueueService, + private messagingService: MessagingService, + ) { + this.#logger = this.apLoggerService.logger; + } + + public async performActivity(actor: CacheableRemoteUser, activity: IObject) { + if (isCollectionOrOrderedCollection(activity)) { + const resolver = this.apResolverService.createResolver(); + for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { + const act = await resolver.resolve(item); + try { + await this.performOneActivity(actor, act); + } catch (err) { + if (err instanceof Error || typeof err === 'string') { + this.#logger.error(err); + } + } + } + } else { + await this.performOneActivity(actor, activity); + } + + // ついでにリモートユーザーの情報が古かったら更新しておく + if (actor.uri) { + if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { + setImmediate(() => { + this.apPersonService.updatePerson(actor.uri!); + }); + } + } + } + + public async performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { + if (actor.isSuspended) return; + + if (isCreate(activity)) { + await this.#create(actor, activity); + } else if (isDelete(activity)) { + await this.#delete(actor, activity); + } else if (isUpdate(activity)) { + await this.#update(actor, activity); + } else if (isRead(activity)) { + await this.#read(actor, activity); + } else if (isFollow(activity)) { + await this.#follow(actor, activity); + } else if (isAccept(activity)) { + await this.#accept(actor, activity); + } else if (isReject(activity)) { + await this.#reject(actor, activity); + } else if (isAdd(activity)) { + await this.#add(actor, activity).catch(err => this.#logger.error(err)); + } else if (isRemove(activity)) { + await this.#remove(actor, activity).catch(err => this.#logger.error(err)); + } else if (isAnnounce(activity)) { + await this.#announce(actor, activity); + } else if (isLike(activity)) { + await this.#like(actor, activity); + } else if (isUndo(activity)) { + await this.#undo(actor, activity); + } else if (isBlock(activity)) { + await this.#block(actor, activity); + } else if (isFlag(activity)) { + await this.#flag(actor, activity); + } else { + this.#logger.warn(`unrecognized activity type: ${(activity as any).type}`); + } + } + + async #follow(actor: CacheableRemoteUser, activity: IFollow): Promise { + const followee = await this.apDbResolverService.getUserFromApId(activity.object); + + if (followee == null) { + return 'skip: followee not found'; + } + + if (followee.host != null) { + return 'skip: フォローしようとしているユーザーはローカルユーザーではありません'; + } + + await this.userFollowingService.follow(actor, followee, activity.id); + return 'ok'; + } + + async #like(actor: CacheableRemoteUser, activity: ILike): Promise { + const targetUri = getApId(activity.object); + + const note = await this.apNoteService.fetchNote(targetUri); + if (!note) return `skip: target note not found ${targetUri}`; + + await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); + + return await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name).catch(e => { + if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { + return 'skip: already reacted'; + } else { + throw e; + } + }).then(() => 'ok'); + } + + async #read(actor: CacheableRemoteUser, activity: IRead): Promise { + const id = await getApId(activity.object); + + if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) { + return `skip: Read to foreign host (${id})`; + } + + const messageId = id.split('/').pop(); + + const message = await this.messagingMessagesRepository.findOneBy({ id: messageId }); + if (message == null) { + return 'skip: message not found'; + } + + if (actor.id !== message.recipientId) { + return 'skip: actor is not a message recipient'; + } + + await this.messagingService.readUserMessagingMessage(message.recipientId!, message.userId, [message.id]); + return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; + } + + async #accept(actor: CacheableRemoteUser, activity: IAccept): Promise { + const uri = activity.id ?? activity; + + this.#logger.info(`Accept: ${uri}`); + + const resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(activity.object).catch(err => { + this.#logger.error(`Resolution failed: ${err}`); + throw err; + }); + + if (isFollow(object)) return await this.#acceptFollow(actor, object); + + return `skip: Unknown Accept type: ${getApType(object)}`; + } + + async #acceptFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある + + const follower = await this.apDbResolverService.getUserFromApId(activity.actor); + + if (follower == null) { + return 'skip: follower not found'; + } + + if (follower.host != null) { + return 'skip: follower is not a local user'; + } + + // relay + const match = activity.id?.match(/follow-relay\/(\w+)/); + if (match) { + return await this.relayService.relayAccepted(match[1]); + } + + await this.userFollowingService.acceptFollowRequest(actor, follower); + return 'ok'; + } + + async #add(actor: CacheableRemoteUser, activity: IAdd): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await this.apNoteService.resolveNote(activity.object); + if (note == null) throw new Error('note not found'); + await this.notePiningService.addPinned(actor, note.id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); + } + + async #announce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + const uri = getApId(activity); + + this.#logger.info(`Announce: ${uri}`); + + const targetUri = getApId(activity.object); + + this.#announceNote(actor, activity, targetUri); + } + + async #announceNote(actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { + const uri = getApId(activity); + + if (actor.isSuspended) { + return; + } + + // アナウンス先をブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return; + + const unlock = await this.appLockService.getApLock(uri); + + try { + // 既に同じURIを持つものが登録されていないかチェック + const exist = await this.apNoteService.fetchNote(uri); + if (exist) { + return; + } + + // Announce対象をresolve + let renote; + try { + renote = await this.apNoteService.resolveNote(targetUri); + } catch (err) { + // 対象が4xxならスキップ + if (err instanceof StatusError) { + if (err.isClientError) { + this.#logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); + return; + } + + this.#logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); + } + throw err; + } + + if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity'; + + this.#logger.info(`Creating the (Re)Note: ${uri}`); + + const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); + + await this.noteCreateService.create(actor, { + createdAt: activity.published ? new Date(activity.published) : null, + renote, + visibility: activityAudience.visibility, + visibleUsers: activityAudience.visibleUsers, + uri, + }); + } finally { + unlock(); + } + } + + async #block(actor: CacheableRemoteUser, activity: IBlock): Promise { + // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず + + const blockee = await this.apDbResolverService.getUserFromApId(activity.object); + + if (blockee == null) { + return 'skip: blockee not found'; + } + + if (blockee.host != null) { + return 'skip: ブロックしようとしているユーザーはローカルユーザーではありません'; + } + + await this.userBlockingService.block(await this.usersRepository.findOneByOrFail({ id: actor.id }), await this.usersRepository.findOneByOrFail({ id: blockee.id })); + return 'ok'; + } + + async #create(actor: CacheableRemoteUser, activity: ICreate): Promise { + const uri = getApId(activity); + + this.#logger.info(`Create: ${uri}`); + + // copy audiences between activity <=> object. + if (typeof activity.object === 'object') { + const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); + const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); + + activity.to = to; + activity.cc = cc; + activity.object.to = to; + activity.object.cc = cc; + } + + // If there is no attributedTo, use Activity actor. + if (typeof activity.object === 'object' && !activity.object.attributedTo) { + activity.object.attributedTo = activity.actor; + } + + const resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(activity.object).catch(e => { + this.#logger.error(`Resolution failed: ${e}`); + throw e; + }); + + if (isPost(object)) { + this.#createNote(resolver, actor, object, false, activity); + } else { + this.#logger.warn(`Unknown type: ${getApType(object)}`); + } + } + + async #createNote(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + const uri = getApId(note); + + if (typeof note === 'object') { + if (actor.uri !== note.attributedTo) { + return 'skip: actor.uri !== note.attributedTo'; + } + + if (typeof note.id === 'string') { + if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { + return 'skip: host in actor.uri !== note.id'; + } + } + } + + const unlock = await this.appLockService.getApLock(uri); + + try { + const exist = await this.apNoteService.fetchNote(note); + if (exist) return 'skip: note exists'; + + await this.apNoteService.createNote(note, resolver, silent); + return 'ok'; + } catch (e) { + if (e instanceof StatusError && e.isClientError) { + return `skip ${e.statusCode}`; + } else { + throw e; + } + } finally { + unlock(); + } + } + + async #delete(actor: CacheableRemoteUser, activity: IDelete): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + // 削除対象objectのtype + let formerType: string | undefined; + + if (typeof activity.object === 'string') { + // typeが不明だけど、どうせ消えてるのでremote resolveしない + formerType = undefined; + } else { + const object = activity.object as IObject; + if (isTombstone(object)) { + formerType = toSingle(object.formerType); + } else { + formerType = toSingle(object.type); + } + } + + const uri = getApId(activity.object); + + // type不明でもactorとobjectが同じならばそれはPersonに違いない + if (!formerType && actor.uri === uri) { + formerType = 'Person'; + } + + // それでもなかったらおそらくNote + if (!formerType) { + formerType = 'Note'; + } + + if (validPost.includes(formerType)) { + return await this.#deleteNote(actor, uri); + } else if (validActor.includes(formerType)) { + return await this.#deleteActor(actor, uri); + } else { + return `Unknown type ${formerType}`; + } + } + + async #deleteActor(actor: CacheableRemoteUser, uri: string): Promise { + this.#logger.info(`Deleting the Actor: ${uri}`); + + if (actor.uri !== uri) { + return `skip: delete actor ${actor.uri} !== ${uri}`; + } + + const user = await this.usersRepository.findOneByOrFail({ id: actor.id }); + if (user.isDeleted) { + this.#logger.info('skip: already deleted'); + } + + const job = await this.queueService.createDeleteAccountJob(actor); + + await this.usersRepository.update(actor.id, { + isDeleted: true, + }); + + return `ok: queued ${job.name} ${job.id}`; + } + + async #deleteNote(actor: CacheableRemoteUser, uri: string): Promise { + this.#logger.info(`Deleting the Note: ${uri}`); + + const unlock = await this.appLockService.getApLock(uri); + + try { + const note = await this.apDbResolverService.getNoteFromApId(uri); + + if (note == null) { + const message = await this.apDbResolverService.getMessageFromApId(uri); + if (message == null) return 'message not found'; + + if (message.userId !== actor.id) { + return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; + } + + await this.messagingService.deleteMessage(message); + + return 'ok: message deleted'; + } + + if (note.userId !== actor.id) { + return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; + } + + await this.noteDeleteService.delete(actor, note); + return 'ok: note deleted'; + } finally { + unlock(); + } + } + + async #flag(actor: CacheableRemoteUser, activity: IFlag): Promise { + // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので + // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する + const uris = getApIds(activity.object); + + const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); + const users = await this.usersRepository.findBy({ + id: In(userIds), + }); + if (users.length < 1) return 'skip'; + + await this.abuseUserReportsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + targetUserId: users[0].id, + targetUserHost: users[0].host, + reporterId: actor.id, + reporterHost: actor.host, + comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, + }); + + return 'ok'; + } + + async #reject(actor: CacheableRemoteUser, activity: IReject): Promise { + const uri = activity.id ?? activity; + + this.#logger.info(`Reject: ${uri}`); + + const resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(activity.object).catch(e => { + this.#logger.error(`Resolution failed: ${e}`); + throw e; + }); + + if (isFollow(object)) return await this.#rejectFollow(actor, object); + + return `skip: Unknown Reject type: ${getApType(object)}`; + } + + async #rejectFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある + + const follower = await this.apDbResolverService.getUserFromApId(activity.actor); + + if (follower == null) { + return 'skip: follower not found'; + } + + if (!this.userEntityService.isLocalUser(follower)) { + return 'skip: follower is not a local user'; + } + + // relay + const match = activity.id?.match(/follow-relay\/(\w+)/); + if (match) { + return await this.relayService.relayRejected(match[1]); + } + + await this.userFollowingService.remoteReject(actor, follower); + return 'ok'; + } + + async #remove(actor: CacheableRemoteUser, activity: IRemove): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await this.apNoteService.resolveNote(activity.object); + if (note == null) throw new Error('note not found'); + await this.notePiningService.removePinned(actor, note.id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); + } + + async #undo(actor: CacheableRemoteUser, activity: IUndo): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id ?? activity; + + this.#logger.info(`Undo: ${uri}`); + + const resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(activity.object).catch(e => { + this.#logger.error(`Resolution failed: ${e}`); + throw e; + }); + + if (isFollow(object)) return await this.#undoFollow(actor, object); + if (isBlock(object)) return await this.#undoBlock(actor, object); + if (isLike(object)) return await this.#undoLike(actor, object); + if (isAnnounce(object)) return await this.#undoAnnounce(actor, object); + if (isAccept(object)) return await this.#undoAccept(actor, object); + + return `skip: unknown object type ${getApType(object)}`; + } + + async #undoAccept(actor: CacheableRemoteUser, activity: IAccept): Promise { + const follower = await this.apDbResolverService.getUserFromApId(activity.object); + if (follower == null) { + return 'skip: follower not found'; + } + + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: actor.id, + }); + + if (following) { + await this.userFollowingService.unfollow(follower, actor); + return 'ok: unfollowed'; + } + + return 'skip: フォローされていない'; + } + + async #undoAnnounce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + const uri = getApId(activity); + + const note = await this.notesRepository.findOneBy({ + uri, + userId: actor.id, + }); + + if (!note) return 'skip: no such Announce'; + + await this.noteDeleteService.delete(actor, note); + return 'ok: deleted'; + } + + async #undoBlock(actor: CacheableRemoteUser, activity: IBlock): Promise { + const blockee = await this.apDbResolverService.getUserFromApId(activity.object); + + if (blockee == null) { + return 'skip: blockee not found'; + } + + if (blockee.host != null) { + return 'skip: ブロック解除しようとしているユーザーはローカルユーザーではありません'; + } + + await this.userBlockingService.unblock(await this.usersRepository.findOneByOrFail({ id: actor.id }), blockee); + return 'ok'; + } + + async #undoFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + const followee = await this.apDbResolverService.getUserFromApId(activity.object); + if (followee == null) { + return 'skip: followee not found'; + } + + if (followee.host != null) { + return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; + } + + const req = await this.followRequestsRepository.findOneBy({ + followerId: actor.id, + followeeId: followee.id, + }); + + const following = await this.followingsRepository.findOneBy({ + followerId: actor.id, + followeeId: followee.id, + }); + + if (req) { + await this.userFollowingService.cancelFollowRequest(followee, actor); + return 'ok: follow request canceled'; + } + + if (following) { + await this.userFollowingService.unfollow(actor, followee); + return 'ok: unfollowed'; + } + + return 'skip: リクエストもフォローもされていない'; + } + + async #undoLike(actor: CacheableRemoteUser, activity: ILike): Promise { + const targetUri = getApId(activity.object); + + const note = await this.apNoteService.fetchNote(targetUri); + if (!note) return `skip: target note not found ${targetUri}`; + + await this.reactionService.delete(actor, note).catch(e => { + if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return; + throw e; + }); + + return 'ok'; + } + + async #update(actor: CacheableRemoteUser, activity: IUpdate): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + return 'skip: invalid actor'; + } + + this.#logger.debug('Update'); + + const resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(activity.object).catch(e => { + this.#logger.error(`Resolution failed: ${e}`); + throw e; + }); + + if (isActor(object)) { + await this.apPersonService.updatePerson(actor.uri!, resolver, object); + return 'ok: Person updated'; + } else if (getApType(object) === 'Question') { + await this.apQuestionService.updateQuestion(object).catch(err => console.error(err)); + return 'ok: Question updated'; + } else { + return `skip: Unknown type: ${getApType(object)}`; + } + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApLoggerService.ts b/packages/backend/src/core/remote/activitypub/ApLoggerService.ts new file mode 100644 index 0000000000..82fd7c5f18 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApLoggerService.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type Logger from '@/logger.js'; +import { RemoteLoggerService } from '@/core/remote/RemoteLoggerService.js'; + +@Injectable() +export class ApLoggerService { + public logger: Logger; + + constructor( + private remoteLoggerService: RemoteLoggerService, + ) { + this.logger = this.remoteLoggerService.logger.createSubLogger('ap', 'magenta'); + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApMfmService.ts b/packages/backend/src/core/remote/activitypub/ApMfmService.ts new file mode 100644 index 0000000000..3c3b98b139 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApMfmService.ts @@ -0,0 +1,30 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as mfm from 'mfm-js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +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'; + +@Injectable() +export class ApMfmService { + constructor( + @Inject(DI.config) + private config: Config, + + private mfmService: MfmService, + ) { + } + + 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); + } + + 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/remote/activitypub/ApRendererService.ts b/packages/backend/src/core/remote/activitypub/ApRendererService.ts new file mode 100644 index 0000000000..b065c1acdd --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApRendererService.ts @@ -0,0 +1,702 @@ +import { createPublicKey } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { In, IsNull } from 'typeorm'; +import { v4 as uuid } from 'uuid'; +import * as mfm from 'mfm-js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; +import type { Relay } from '@/models/entities/Relay.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import type { Poll } from '@/models/entities/Poll.js'; +import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import type { PollVote } from '@/models/entities/PollVote.js'; +import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; +import { MfmService } from '@/core/MfmService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import type { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { LdSignatureService } from './LdSignatureService.js'; +import { ApMfmService } from './ApMfmService.js'; +import type { IActivity } from './type.js'; +import type { IIdentifier } from './models/identifier.js'; + +@Injectable() +export class ApRendererService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + private ldSignatureService: LdSignatureService, + private userKeypairStoreService: UserKeypairStoreService, + private apMfmService: ApMfmService, + private mfmService: MfmService, + ) { + } + + public renderAccept(object: any, user: { id: User['id']; host: null }) { + return { + type: 'Accept', + actor: `${this.config.url}/users/${user.id}`, + object, + }; + } + + public renderAdd(user: ILocalUser, target: any, object: any) { + return { + type: 'Add', + actor: `${this.config.url}/users/${user.id}`, + target, + object, + }; + } + + public renderAnnounce(object: any, note: Note) { + const attributedTo = `${this.config.url}/users/${note.userId}`; + + let to: string[] = []; + let cc: string[] = []; + + if (note.visibility === 'public') { + to = ['https://www.w3.org/ns/activitystreams#Public']; + cc = [`${attributedTo}/followers`]; + } else if (note.visibility === 'home') { + to = [`${attributedTo}/followers`]; + cc = ['https://www.w3.org/ns/activitystreams#Public']; + } else { + return null; + } + + return { + id: `${this.config.url}/notes/${note.id}/activity`, + actor: `${this.config.url}/users/${note.userId}`, + type: 'Announce', + published: note.createdAt.toISOString(), + to, + cc, + object, + }; + } + + /** + * Renders a block into its ActivityPub representation. + * + * @param block The block to be rendered. The blockee relation must be loaded. + */ + public renderBlock(block: Blocking) { + if (block.blockee?.uri == null) { + throw new Error('renderBlock: missing blockee uri'); + } + + return { + type: 'Block', + id: `${this.config.url}/blocks/${block.id}`, + actor: `${this.config.url}/users/${block.blockerId}`, + object: block.blockee.uri, + }; + } + + public renderCreate(object: any, note: Note) { + const activity = { + id: `${this.config.url}/notes/${note.id}/activity`, + actor: `${this.config.url}/users/${note.userId}`, + type: 'Create', + published: note.createdAt.toISOString(), + object, + } as any; + + if (object.to) activity.to = object.to; + if (object.cc) activity.cc = object.cc; + + return activity; + } + + public renderDelete(object: any, user: { id: User['id']; host: null }) { + return { + type: 'Delete', + actor: `${this.config.url}/users/${user.id}`, + object, + published: new Date().toISOString(), + }; + } + + public renderDocument(file: DriveFile) { + return { + type: 'Document', + mediaType: file.type, + url: this.driveFileEntityService.getPublicUrl(file), + name: file.comment, + }; + } + + public renderEmoji(emoji: Emoji) { + return { + id: `${this.config.url}/emojis/${emoji.name}`, + type: 'Emoji', + name: `:${emoji.name}:`, + updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString, + icon: { + type: 'Image', + mediaType: emoji.type ?? 'image/png', + url: emoji.publicUrl ?? emoji.originalUrl, // ?? emoji.originalUrl してるのは後方互換性のため + }, + }; + } + + // to anonymise reporters, the reporting actor must be a system user + // object has to be a uri or array of uris + public renderFlag(user: ILocalUser, object: [string], content: string) { + return { + type: 'Flag', + actor: `${this.config.url}/users/${user.id}`, + content, + object, + }; + } + + public renderFollowRelay(relay: Relay, relayActor: ILocalUser) { + const follow = { + id: `${this.config.url}/activities/follow-relay/${relay.id}`, + type: 'Follow', + actor: `${this.config.url}/users/${relayActor.id}`, + object: 'https://www.w3.org/ns/activitystreams#Public', + }; + + return follow; + } + + /** + * Convert (local|remote)(Follower|Followee)ID to URL + * @param id Follower|Followee ID + */ + 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; + } + + public renderFollow( + follower: { id: User['id']; host: User['host']; uri: User['host'] }, + followee: { id: User['id']; host: User['host']; uri: User['host'] }, + requestId?: string, + ) { + const follow = { + id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, + type: 'Follow', + actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri, + object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri, + } as any; + + return follow; + } + + public renderHashtag(tag: string) { + return { + type: 'Hashtag', + href: `${this.config.url}/tags/${encodeURIComponent(tag)}`, + name: `#${tag}`, + }; + } + + public renderImage(file: DriveFile) { + return { + type: 'Image', + url: this.driveFileEntityService.getPublicUrl(file), + sensitive: file.isSensitive, + name: file.comment, + }; + } + + public renderKey(user: ILocalUser, key: UserKeypair, postfix?: string) { + return { + id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, + type: 'Key', + owner: `${this.config.url}/users/${user.id}`, + publicKeyPem: createPublicKey(key.publicKey).export({ + type: 'spki', + format: 'pem', + }), + }; + } + + public async renderLike(noteReaction: NoteReaction, note: Note) { + const reaction = noteReaction.reaction; + + const object = { + type: 'Like', + id: `${this.config.url}/likes/${noteReaction.id}`, + actor: `${this.config.url}/users/${noteReaction.userId}`, + object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, + content: reaction, + _misskey_reaction: reaction, + } as any; + + if (reaction.startsWith(':')) { + const name = reaction.replace(/:/g, ''); + const emoji = await this.emojisRepository.findOneBy({ + name, + host: IsNull(), + }); + + if (emoji) object.tag = [this.renderEmoji(emoji)]; + } + + return object; + } + + public renderMention(mention: User) { + return { + type: 'Mention', + href: this.userEntityService.isRemoteUser(mention) ? mention.uri : `${this.config.url}/users/${(mention as ILocalUser).id}`, + name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, + }; + } + + public async renderNote(note: Note, dive = true, isTalk = false): Promise> { + const getPromisedFiles = async (ids: string[]) => { + if (!ids || ids.length === 0) return []; + const items = await this.driveFilesRepository.findBy({ id: In(ids) }); + return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; + }; + + let inReplyTo; + let inReplyToNote: Note | null; + + if (note.replyId) { + inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); + + if (inReplyToNote != null) { + const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); + + if (inReplyToUser != null) { + if (inReplyToNote.uri) { + inReplyTo = inReplyToNote.uri; + } else { + if (dive) { + inReplyTo = await this.renderNote(inReplyToNote, false); + } else { + inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`; + } + } + } + } + } else { + inReplyTo = null; + } + + let quote; + + if (note.renoteId) { + const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); + + if (renote) { + quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`; + } + } + + const attributedTo = `${this.config.url}/users/${note.userId}`; + + const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + + let to: string[] = []; + let cc: string[] = []; + + if (note.visibility === 'public') { + to = ['https://www.w3.org/ns/activitystreams#Public']; + cc = [`${attributedTo}/followers`].concat(mentions); + } else if (note.visibility === 'home') { + to = [`${attributedTo}/followers`]; + cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); + } else if (note.visibility === 'followers') { + to = [`${attributedTo}/followers`]; + cc = mentions; + } else { + to = mentions; + } + + const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({ + id: In(note.mentions), + }) : []; + + const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); + const mentionTags = mentionedUsers.map(u => this.renderMention(u)); + + const files = await getPromisedFiles(note.fileIds); + + const text = note.text ?? ''; + let poll: Poll | null = null; + + if (note.hasPoll) { + poll = await this.pollsRepository.findOneBy({ noteId: note.id }); + } + + let apText = text; + + if (quote) { + apText += `\n\nRE: ${quote}`; + } + + const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; + + const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: apText, + })); + + const emojis = await this.#getEmojis(note.emojis); + const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); + + const tag = [ + ...hashtagTags, + ...mentionTags, + ...apemojis, + ]; + + const asPoll = poll ? { + type: 'Question', + content: this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: text, + })), + [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ + type: 'Note', + name: text, + replies: { + type: 'Collection', + totalItems: poll!.votes[i], + }, + })), + } : {}; + + const asTalk = isTalk ? { + _misskey_talk: true, + } : {}; + + return { + id: `${this.config.url}/notes/${note.id}`, + type: 'Note', + attributedTo, + summary, + content, + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, + _misskey_quote: quote, + quoteUrl: quote, + published: note.createdAt.toISOString(), + to, + cc, + inReplyTo, + attachment: files.map(x => this.renderDocument(x)), + sensitive: note.cw != null || files.some(file => file.isSensitive), + tag, + ...asPoll, + ...asTalk, + }; + } + + public async renderPerson(user: ILocalUser) { + const id = `${this.config.url}/users/${user.id}`; + const isSystem = !!user.username.match(/\./); + + const [avatar, banner, profile] = await Promise.all([ + user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), + user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), + this.userProfilesRepository.findOneByOrFail({ userId: user.id }), + ]); + + const attachment: { + type: 'PropertyValue', + name: string, + value: string, + identifier?: IIdentifier, + }[] = []; + + if (profile.fields) { + for (const field of profile.fields) { + attachment.push({ + type: 'PropertyValue', + name: field.name, + value: (field.value != null && field.value.match(/^https?:/)) + ? `${new URL(field.value).href}` + : field.value, + }); + } + } + + const emojis = await this.#getEmojis(user.emojis); + const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); + + const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); + + const tag = [ + ...apemojis, + ...hashtagTags, + ]; + + const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); + + const person = { + type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + followers: `${id}/followers`, + following: `${id}/following`, + featured: `${id}/collections/featured`, + sharedInbox: `${this.config.url}/inbox`, + endpoints: { sharedInbox: `${this.config.url}/inbox` }, + url: `${this.config.url}/@${user.username}`, + preferredUsername: user.username, + name: user.name, + summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, + icon: avatar ? this.renderImage(avatar) : null, + image: banner ? this.renderImage(banner) : null, + tag, + manuallyApprovesFollowers: user.isLocked, + discoverable: !!user.isExplorable, + publicKey: this.renderKey(user, keypair, '#main-key'), + isCat: user.isCat, + attachment: attachment.length ? attachment : undefined, + } as any; + + if (profile.birthday) { + person['vcard:bday'] = profile.birthday; + } + + if (profile.location) { + person['vcard:Address'] = profile.location; + } + + return person; + } + + public async renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { + const question = { + type: 'Question', + id: `${this.config.url}/questions/${note.id}`, + actor: `${this.config.url}/users/${user.id}`, + content: note.text ?? '', + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ + name: text, + _misskey_votes: poll.votes[i], + replies: { + type: 'Collection', + totalItems: poll.votes[i], + }, + })), + }; + + return question; + } + + public renderRead(user: { id: User['id'] }, message: MessagingMessage) { + return { + type: 'Read', + actor: `${this.config.url}/users/${user.id}`, + object: message.uri, + }; + } + + public renderReject(object: any, user: { id: User['id'] }) { + return { + type: 'Reject', + actor: `${this.config.url}/users/${user.id}`, + object, + }; + } + + public renderRemove(user: { id: User['id'] }, target: any, object: any) { + return { + type: 'Remove', + actor: `${this.config.url}/users/${user.id}`, + target, + object, + }; + } + + public renderTombstone(id: string) { + return { + id, + type: 'Tombstone', + }; + } + + 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; + + return { + type: 'Undo', + ...(id ? { id } : {}), + actor: `${this.config.url}/users/${user.id}`, + object, + published: new Date().toISOString(), + }; + } + + public renderUpdate(object: any, user: { id: User['id'] }) { + const activity = { + id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, + actor: `${this.config.url}/users/${user.id}`, + type: 'Update', + to: ['https://www.w3.org/ns/activitystreams#Public'], + object, + published: new Date().toISOString(), + } as any; + + return activity; + } + + 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`, + actor: `${this.config.url}/users/${user.id}`, + type: 'Create', + to: [pollOwner.uri], + published: new Date().toISOString(), + object: { + id: `${this.config.url}/users/${user.id}#votes/${vote.id}`, + type: 'Note', + attributedTo: `${this.config.url}/users/${user.id}`, + to: [pollOwner.uri], + inReplyTo: note.uri, + name: poll.choices[vote.choice], + }, + }; + } + + public renderActivity(x: any): IActivity | null { + if (x == null) return null; + + if (typeof x === 'object' && x.id == null) { + x.id = `${this.config.url}/${uuid()}`; + } + + return Object.assign({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + // as non-standards + manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', + sensitive: 'as:sensitive', + Hashtag: 'as:Hashtag', + quoteUrl: 'as:quoteUrl', + // Mastodon + toot: 'http://joinmastodon.org/ns#', + Emoji: 'toot:Emoji', + featured: 'toot:featured', + discoverable: 'toot:discoverable', + // schema + schema: 'http://schema.org#', + PropertyValue: 'schema:PropertyValue', + value: 'schema:value', + // Misskey + misskey: 'https://misskey-hub.net/ns#', + '_misskey_content': 'misskey:_misskey_content', + '_misskey_quote': 'misskey:_misskey_quote', + '_misskey_reaction': 'misskey:_misskey_reaction', + '_misskey_votes': 'misskey:_misskey_votes', + '_misskey_talk': 'misskey:_misskey_talk', + 'isCat': 'misskey:isCat', + // vcard + vcard: 'http://www.w3.org/2006/vcard/ns#', + }, + ], + }, x); + } + + public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise { + const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); + + const ldSignature = this.ldSignatureService.use(); + ldSignature.debug = false; + activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); + + return activity; + } + + /** + * Render OrderedCollectionPage + * @param id URL of self + * @param totalItems Number of total items + * @param orderedItems Items + * @param partOf URL of base + * @param prev URL of prev page (optional) + * @param next URL of next page (optional) + */ + public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { + const page = { + id, + partOf, + type: 'OrderedCollectionPage', + totalItems, + orderedItems, + } as any; + + if (prev) page.prev = prev; + if (next) page.next = next; + + return page; + } + + /** + * Render OrderedCollection + * @param id URL of self + * @param totalItems Total number of items + * @param first URL of first page (optional) + * @param last URL of last page (optional) + * @param orderedItems attached objects (optional) + */ + public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: Record[]) { + const page: any = { + id, + type: 'OrderedCollection', + totalItems, + }; + + if (first) page.first = first; + if (last) page.last = last; + if (orderedItems) page.orderedItems = orderedItems; + + return page; + } + + async #getEmojis(names: string[]): Promise { + if (names == null || names.length === 0) return []; + + const emojis = await Promise.all( + names.map(name => this.emojisRepository.findOneBy({ + name, + host: IsNull(), + })), + ); + + return emojis.filter(emoji => emoji != null) as Emoji[]; + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApRequestService.ts b/packages/backend/src/core/remote/activitypub/ApRequestService.ts new file mode 100644 index 0000000000..a449a77dc8 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApRequestService.ts @@ -0,0 +1,182 @@ +import * as crypto from 'node:crypto'; +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type { User } from '@/models/entities/User.js'; +import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; + +type Request = { + url: string; + method: string; + headers: Record; +}; + +type Signed = { + request: Request; + signingString: string; + signature: string; + signatureHeader: string; +}; + +type PrivateKey = { + privateKeyPem: string; + keyId: string; +}; + +@Injectable() +export class ApRequestService { + constructor( + @Inject(DI.config) + private config: Config, + + private userKeypairStoreService: UserKeypairStoreService, + private httpRequestService: HttpRequestService, + ) { + } + + #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')}`; + + const request: Request = { + url: u.href, + method: 'POST', + headers: this.#objectAssignWithLcKey({ + 'Date': new Date().toUTCString(), + 'Host': u.hostname, + 'Content-Type': 'application/activity+json', + 'Digest': digestHeader, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); + + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } + + #createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { + const u = new URL(args.url); + + const request: Request = { + url: u.href, + method: 'GET', + headers: this.#objectAssignWithLcKey({ + 'Accept': 'application/activity+json, application/ld+json', + 'Date': new Date().toUTCString(), + 'Host': new URL(args.url).hostname, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } + + #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'); + const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; + + request.headers = this.#objectAssignWithLcKey(request.headers, { + Signature: signatureHeader, + }); + + return { + request, + signingString, + signature, + signatureHeader, + }; + } + + #genSigningString(request: Request, includeHeaders: string[]): string { + request.headers = this.#lcObjectKey(request.headers); + + const results: string[] = []; + + for (const key of includeHeaders.map(x => x.toLowerCase())) { + if (key === '(request-target)') { + results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); + } else { + results.push(`${key}: ${request.headers[key]}`); + } + } + + return results.join('\n'); + } + + #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; + } + + #objectAssignWithLcKey(a: Record, b: Record): Record { + return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b)); + } + + public async signedPost(user: { id: User['id'] }, url: string, object: any) { + const body = JSON.stringify(object); + + const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); + + const req = this.#createSignedPost({ + key: { + privateKeyPem: keypair.privateKey, + keyId: `${this.config.url}/users/${user.id}#main-key`, + }, + url, + body, + additionalHeaders: { + 'User-Agent': this.config.userAgent, + }, + }); + + await this.httpRequestService.getResponse({ + url, + method: req.request.method, + headers: req.request.headers, + body, + }); + } + + /** + * Get AP object with http-signature + * @param user http-signature user + * @param url URL to fetch + */ + public async signedGet(url: string, user: { id: User['id'] }) { + const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); + + const req = this.#createSignedGet({ + key: { + privateKeyPem: keypair.privateKey, + keyId: `${this.config.url}/users/${user.id}#main-key`, + }, + url, + additionalHeaders: { + 'User-Agent': this.config.userAgent, + }, + }); + + const res = await this.httpRequestService.getResponse({ + url, + method: req.request.method, + headers: req.request.headers, + }); + + return await res.json(); + } +} diff --git a/packages/backend/src/core/remote/activitypub/ApResolverService.ts b/packages/backend/src/core/remote/activitypub/ApResolverService.ts new file mode 100644 index 0000000000..9d8e177758 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/ApResolverService.ts @@ -0,0 +1,190 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +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 { 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; + + constructor( + private config: Config, + private usersRepository: UsersRepository, + private notesRepository: NotesRepository, + private pollsRepository: PollsRepository, + private noteReactionsRepository: NoteReactionsRepository, + private utilityService: UtilityService, + private instanceActorService: InstanceActorService, + private metaService: MetaService, + private apRequestService: ApRequestService, + private httpRequestService: HttpRequestService, + private apRendererService: ApRendererService, + private apDbResolverService: ApDbResolverService, + ) { + this.history = new Set(); + } + + public getHistory(): string[] { + return Array.from(this.history); + } + + public async resolveCollection(value: string | IObject): Promise { + const collection = typeof value === 'string' + ? await this.resolve(value) + : value; + + if (isCollectionOrOrderedCollection(collection)) { + return collection; + } else { + throw new Error(`unrecognized collection type: ${collection.type}`); + } + } + + public async resolve(value: string | IObject): Promise { + if (value == null) { + throw new Error('resolvee is null (or undefined)'); + } + + if (typeof value !== 'string') { + return value; + } + + if (value.includes('#')) { + // URLs with fragment parts cannot be resolved correctly because + // the fragment part does not get transmitted over HTTP(S). + // Avoid strange behaviour by not trying to resolve these at all. + throw new Error(`cannot resolve URL with fragment: ${value}`); + } + + if (this.history.has(value)) { + throw new Error('cannot resolve already resolved one'); + } + + this.history.add(value); + + const host = this.utilityService.extractDbHost(value); + if (this.utilityService.isSelfHost(host)) { + return await this.resolveLocal(value); + } + + const meta = await this.metaService.fetch(); + if (meta.blockedHosts.includes(host)) { + throw new Error('Instance is blocked'); + } + + if (this.config.signToActivityPubGet && !this.user) { + this.user = await this.instanceActorService.getInstanceActor(); + } + + const object = (this.user + ? await this.apRequestService.signedGet(value, this.user) + : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; + + if (object == null || ( + Array.isArray(object['@context']) ? + !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : + object['@context'] !== 'https://www.w3.org/ns/activitystreams' + )) { + throw new Error('invalid response'); + } + + return object; + } + + private resolveLocal(url: string): Promise { + const parsed = this.apDbResolverService.parseUri(url); + if (!parsed.local) throw new Error('resolveLocal: not local'); + + switch (parsed.type) { + case 'notes': + return this.notesRepository.findOneByOrFail({ id: parsed.id }) + .then(note => { + if (parsed.rest === 'activity') { + // this refers to the create activity and not the note itself + return this.apRendererService.renderActivity(this.apRendererService.renderCreate(this.apRendererService.renderNote(note))); + } else { + return this.apRendererService.renderNote(note); + } + }); + case 'users': + return this.usersRepository.findOneByOrFail({ id: parsed.id }) + .then(user => this.apRendererService.renderPerson(user as ILocalUser)); + case 'questions': + // Polls are indexed by the note they are attached to. + return Promise.all([ + this.notesRepository.findOneByOrFail({ id: parsed.id }), + this.pollsRepository.findOneByOrFail({ noteId: parsed.id }), + ]) + .then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)); + case 'likes': + return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(reaction => + this.apRendererService.renderActivity(this.apRendererService.renderLike(reaction, { uri: null }))); + case 'follows': + // rest should be + if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); + + return Promise.all( + [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), + ) + .then(([follower, followee]) => this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee, url))); + default: + throw new Error(`resolveLocal: type ${type} unhandled`); + } + } +} diff --git a/packages/backend/src/core/remote/activitypub/LdSignatureService.ts b/packages/backend/src/core/remote/activitypub/LdSignatureService.ts new file mode 100644 index 0000000000..ea0d2daf3d --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/LdSignatureService.ts @@ -0,0 +1,150 @@ +import * as crypto from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import jsonld from 'jsonld'; +import fetch from 'node-fetch'; +import { HttpRequestService } from '@/core/HttpRequestService.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; + public loderTimeout = 10 * 1000; + + constructor( + private httpRequestService: HttpRequestService, + ) { + } + + public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { + const options = { + type: 'RsaSignature2017', + creator, + domain, + nonce: crypto.randomBytes(16).toString('hex'), + created: (created ?? new Date()).toISOString(), + } as { + type: string; + creator: string; + domain?: string; + nonce: string; + created: string; + }; + + if (!domain) { + delete options.domain; + } + + const toBeSigned = await this.createVerifyData(data, options); + + const signer = crypto.createSign('sha256'); + signer.update(toBeSigned); + signer.end(); + + const signature = signer.sign(privateKey); + + return { + ...data, + signature: { + ...options, + signatureValue: signature.toString('base64'), + }, + }; + } + + public async verifyRsaSignature2017(data: any, publicKey: string): Promise { + const toBeSigned = await this.createVerifyData(data, data.signature); + const verifier = crypto.createVerify('sha256'); + verifier.update(toBeSigned); + return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); + } + + public async createVerifyData(data: any, options: any) { + const transformedOptions = { + ...options, + '@context': 'https://w3id.org/identity/v1', + }; + delete transformedOptions['type']; + delete transformedOptions['id']; + delete transformedOptions['signatureValue']; + const canonizedOptions = await this.normalize(transformedOptions); + const optionsHash = this.sha256(canonizedOptions); + const transformedData = { ...data }; + delete transformedData['signature']; + const cannonidedData = await this.normalize(transformedData); + if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); + const documentHash = this.sha256(cannonidedData); + const verifyData = `${optionsHash}${documentHash}`; + return verifyData; + } + + public async normalize(data: any) { + const customLoader = this.getLoader(); + return await jsonld.normalize(data, { + documentLoader: customLoader, + }); + } + + private getLoader() { + return async (url: string): Promise => { + if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; + + if (this.preLoad) { + if (url in CONTEXTS) { + if (this.debug) console.debug(`HIT: ${url}`); + return { + contextUrl: null, + document: CONTEXTS[url], + documentUrl: url, + }; + } + } + + if (this.debug) console.debug(`MISS: ${url}`); + const document = await this.fetchDocument(url); + return { + contextUrl: null, + document: document, + documentUrl: url, + }; + }; + } + + private async fetchDocument(url: string) { + const json = await fetch(url, { + headers: { + Accept: 'application/ld+json, application/json', + }, + // TODO + //timeout: this.loderTimeout, + agent: u => u.protocol === 'http:' ? this.httpRequestService.httpAgent : this.httpRequestService.httpsAgent, + }).then(res => { + if (!res.ok) { + throw `${res.status} ${res.statusText}`; + } else { + return res.json(); + } + }); + + return json; + } + + public sha256(data: string): string { + const hash = crypto.createHash('sha256'); + hash.update(data); + return hash.digest('hex'); + } +} diff --git a/packages/backend/src/core/remote/activitypub/misc/contexts.ts b/packages/backend/src/core/remote/activitypub/misc/contexts.ts new file mode 100644 index 0000000000..aee0d3629c --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/misc/contexts.ts @@ -0,0 +1,526 @@ +/* eslint:disable:quotemark indent */ +const id_v1 = { + '@context': { + 'id': '@id', + 'type': '@type', + + 'cred': 'https://w3id.org/credentials#', + 'dc': 'http://purl.org/dc/terms/', + 'identity': 'https://w3id.org/identity#', + 'perm': 'https://w3id.org/permissions#', + 'ps': 'https://w3id.org/payswarm#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'sec': 'https://w3id.org/security#', + 'schema': 'http://schema.org/', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + + 'Group': 'https://www.w3.org/ns/activitystreams#Group', + + 'claim': { '@id': 'cred:claim', '@type': '@id' }, + 'credential': { '@id': 'cred:credential', '@type': '@id' }, + 'issued': { '@id': 'cred:issued', '@type': 'xsd:dateTime' }, + 'issuer': { '@id': 'cred:issuer', '@type': '@id' }, + 'recipient': { '@id': 'cred:recipient', '@type': '@id' }, + 'Credential': 'cred:Credential', + 'CryptographicKeyCredential': 'cred:CryptographicKeyCredential', + + 'about': { '@id': 'schema:about', '@type': '@id' }, + 'address': { '@id': 'schema:address', '@type': '@id' }, + 'addressCountry': 'schema:addressCountry', + 'addressLocality': 'schema:addressLocality', + 'addressRegion': 'schema:addressRegion', + 'comment': 'rdfs:comment', + 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, + 'creator': { '@id': 'dc:creator', '@type': '@id' }, + 'description': 'schema:description', + 'email': 'schema:email', + 'familyName': 'schema:familyName', + 'givenName': 'schema:givenName', + 'image': { '@id': 'schema:image', '@type': '@id' }, + 'label': 'rdfs:label', + 'name': 'schema:name', + 'postalCode': 'schema:postalCode', + 'streetAddress': 'schema:streetAddress', + 'title': 'dc:title', + 'url': { '@id': 'schema:url', '@type': '@id' }, + 'Person': 'schema:Person', + 'PostalAddress': 'schema:PostalAddress', + 'Organization': 'schema:Organization', + + 'identityService': { '@id': 'identity:identityService', '@type': '@id' }, + 'idp': { '@id': 'identity:idp', '@type': '@id' }, + 'Identity': 'identity:Identity', + + 'paymentProcessor': 'ps:processor', + 'preferences': { '@id': 'ps:preferences', '@type': '@vocab' }, + + 'cipherAlgorithm': 'sec:cipherAlgorithm', + 'cipherData': 'sec:cipherData', + 'cipherKey': 'sec:cipherKey', + 'digestAlgorithm': 'sec:digestAlgorithm', + 'digestValue': 'sec:digestValue', + 'domain': 'sec:domain', + 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + 'initializationVector': 'sec:initializationVector', + 'member': { '@id': 'schema:member', '@type': '@id' }, + 'memberOf': { '@id': 'schema:memberOf', '@type': '@id' }, + 'nonce': 'sec:nonce', + 'normalizationAlgorithm': 'sec:normalizationAlgorithm', + 'owner': { '@id': 'sec:owner', '@type': '@id' }, + 'password': 'sec:password', + 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, + 'privateKeyPem': 'sec:privateKeyPem', + 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, + 'publicKeyPem': 'sec:publicKeyPem', + 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, + 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, + 'signature': 'sec:signature', + 'signatureAlgorithm': 'sec:signatureAlgorithm', + 'signatureValue': 'sec:signatureValue', + 'CryptographicKey': 'sec:Key', + 'EncryptedMessage': 'sec:EncryptedMessage', + 'GraphSignature2012': 'sec:GraphSignature2012', + 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', + + 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, + 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, + }, +}; + +const security_v1 = { + '@context': { + 'id': '@id', + 'type': '@type', + + 'dc': 'http://purl.org/dc/terms/', + 'sec': 'https://w3id.org/security#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + + 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016', + 'Ed25519Signature2018': 'sec:Ed25519Signature2018', + 'EncryptedMessage': 'sec:EncryptedMessage', + 'GraphSignature2012': 'sec:GraphSignature2012', + 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', + 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016', + 'CryptographicKey': 'sec:Key', + + 'authenticationTag': 'sec:authenticationTag', + 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm', + 'cipherAlgorithm': 'sec:cipherAlgorithm', + 'cipherData': 'sec:cipherData', + 'cipherKey': 'sec:cipherKey', + 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, + 'creator': { '@id': 'dc:creator', '@type': '@id' }, + 'digestAlgorithm': 'sec:digestAlgorithm', + 'digestValue': 'sec:digestValue', + 'domain': 'sec:domain', + 'encryptionKey': 'sec:encryptionKey', + 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + 'initializationVector': 'sec:initializationVector', + 'iterationCount': 'sec:iterationCount', + 'nonce': 'sec:nonce', + 'normalizationAlgorithm': 'sec:normalizationAlgorithm', + 'owner': { '@id': 'sec:owner', '@type': '@id' }, + 'password': 'sec:password', + 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, + 'privateKeyPem': 'sec:privateKeyPem', + 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, + 'publicKeyBase58': 'sec:publicKeyBase58', + 'publicKeyPem': 'sec:publicKeyPem', + 'publicKeyWif': 'sec:publicKeyWif', + 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, + 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, + 'salt': 'sec:salt', + 'signature': 'sec:signature', + 'signatureAlgorithm': 'sec:signingAlgorithm', + 'signatureValue': 'sec:signatureValue', + }, +}; + +const activitystreams = { + '@context': { + '@vocab': '_:', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + 'as': 'https://www.w3.org/ns/activitystreams#', + 'ldp': 'http://www.w3.org/ns/ldp#', + 'vcard': 'http://www.w3.org/2006/vcard/ns#', + 'id': '@id', + 'type': '@type', + 'Accept': 'as:Accept', + 'Activity': 'as:Activity', + 'IntransitiveActivity': 'as:IntransitiveActivity', + 'Add': 'as:Add', + 'Announce': 'as:Announce', + 'Application': 'as:Application', + 'Arrive': 'as:Arrive', + 'Article': 'as:Article', + 'Audio': 'as:Audio', + 'Block': 'as:Block', + 'Collection': 'as:Collection', + 'CollectionPage': 'as:CollectionPage', + 'Relationship': 'as:Relationship', + 'Create': 'as:Create', + 'Delete': 'as:Delete', + 'Dislike': 'as:Dislike', + 'Document': 'as:Document', + 'Event': 'as:Event', + 'Follow': 'as:Follow', + 'Flag': 'as:Flag', + 'Group': 'as:Group', + 'Ignore': 'as:Ignore', + 'Image': 'as:Image', + 'Invite': 'as:Invite', + 'Join': 'as:Join', + 'Leave': 'as:Leave', + 'Like': 'as:Like', + 'Link': 'as:Link', + 'Mention': 'as:Mention', + 'Note': 'as:Note', + 'Object': 'as:Object', + 'Offer': 'as:Offer', + 'OrderedCollection': 'as:OrderedCollection', + 'OrderedCollectionPage': 'as:OrderedCollectionPage', + 'Organization': 'as:Organization', + 'Page': 'as:Page', + 'Person': 'as:Person', + 'Place': 'as:Place', + 'Profile': 'as:Profile', + 'Question': 'as:Question', + 'Reject': 'as:Reject', + 'Remove': 'as:Remove', + 'Service': 'as:Service', + 'TentativeAccept': 'as:TentativeAccept', + 'TentativeReject': 'as:TentativeReject', + 'Tombstone': 'as:Tombstone', + 'Undo': 'as:Undo', + 'Update': 'as:Update', + 'Video': 'as:Video', + 'View': 'as:View', + 'Listen': 'as:Listen', + 'Read': 'as:Read', + 'Move': 'as:Move', + 'Travel': 'as:Travel', + 'IsFollowing': 'as:IsFollowing', + 'IsFollowedBy': 'as:IsFollowedBy', + 'IsContact': 'as:IsContact', + 'IsMember': 'as:IsMember', + 'subject': { + '@id': 'as:subject', + '@type': '@id', + }, + 'relationship': { + '@id': 'as:relationship', + '@type': '@id', + }, + 'actor': { + '@id': 'as:actor', + '@type': '@id', + }, + 'attributedTo': { + '@id': 'as:attributedTo', + '@type': '@id', + }, + 'attachment': { + '@id': 'as:attachment', + '@type': '@id', + }, + 'bcc': { + '@id': 'as:bcc', + '@type': '@id', + }, + 'bto': { + '@id': 'as:bto', + '@type': '@id', + }, + 'cc': { + '@id': 'as:cc', + '@type': '@id', + }, + 'context': { + '@id': 'as:context', + '@type': '@id', + }, + 'current': { + '@id': 'as:current', + '@type': '@id', + }, + 'first': { + '@id': 'as:first', + '@type': '@id', + }, + 'generator': { + '@id': 'as:generator', + '@type': '@id', + }, + 'icon': { + '@id': 'as:icon', + '@type': '@id', + }, + 'image': { + '@id': 'as:image', + '@type': '@id', + }, + 'inReplyTo': { + '@id': 'as:inReplyTo', + '@type': '@id', + }, + 'items': { + '@id': 'as:items', + '@type': '@id', + }, + 'instrument': { + '@id': 'as:instrument', + '@type': '@id', + }, + 'orderedItems': { + '@id': 'as:items', + '@type': '@id', + '@container': '@list', + }, + 'last': { + '@id': 'as:last', + '@type': '@id', + }, + 'location': { + '@id': 'as:location', + '@type': '@id', + }, + 'next': { + '@id': 'as:next', + '@type': '@id', + }, + 'object': { + '@id': 'as:object', + '@type': '@id', + }, + 'oneOf': { + '@id': 'as:oneOf', + '@type': '@id', + }, + 'anyOf': { + '@id': 'as:anyOf', + '@type': '@id', + }, + 'closed': { + '@id': 'as:closed', + '@type': 'xsd:dateTime', + }, + 'origin': { + '@id': 'as:origin', + '@type': '@id', + }, + 'accuracy': { + '@id': 'as:accuracy', + '@type': 'xsd:float', + }, + 'prev': { + '@id': 'as:prev', + '@type': '@id', + }, + 'preview': { + '@id': 'as:preview', + '@type': '@id', + }, + 'replies': { + '@id': 'as:replies', + '@type': '@id', + }, + 'result': { + '@id': 'as:result', + '@type': '@id', + }, + 'audience': { + '@id': 'as:audience', + '@type': '@id', + }, + 'partOf': { + '@id': 'as:partOf', + '@type': '@id', + }, + 'tag': { + '@id': 'as:tag', + '@type': '@id', + }, + 'target': { + '@id': 'as:target', + '@type': '@id', + }, + 'to': { + '@id': 'as:to', + '@type': '@id', + }, + 'url': { + '@id': 'as:url', + '@type': '@id', + }, + 'altitude': { + '@id': 'as:altitude', + '@type': 'xsd:float', + }, + 'content': 'as:content', + 'contentMap': { + '@id': 'as:content', + '@container': '@language', + }, + 'name': 'as:name', + 'nameMap': { + '@id': 'as:name', + '@container': '@language', + }, + 'duration': { + '@id': 'as:duration', + '@type': 'xsd:duration', + }, + 'endTime': { + '@id': 'as:endTime', + '@type': 'xsd:dateTime', + }, + 'height': { + '@id': 'as:height', + '@type': 'xsd:nonNegativeInteger', + }, + 'href': { + '@id': 'as:href', + '@type': '@id', + }, + 'hreflang': 'as:hreflang', + 'latitude': { + '@id': 'as:latitude', + '@type': 'xsd:float', + }, + 'longitude': { + '@id': 'as:longitude', + '@type': 'xsd:float', + }, + 'mediaType': 'as:mediaType', + 'published': { + '@id': 'as:published', + '@type': 'xsd:dateTime', + }, + 'radius': { + '@id': 'as:radius', + '@type': 'xsd:float', + }, + 'rel': 'as:rel', + 'startIndex': { + '@id': 'as:startIndex', + '@type': 'xsd:nonNegativeInteger', + }, + 'startTime': { + '@id': 'as:startTime', + '@type': 'xsd:dateTime', + }, + 'summary': 'as:summary', + 'summaryMap': { + '@id': 'as:summary', + '@container': '@language', + }, + 'totalItems': { + '@id': 'as:totalItems', + '@type': 'xsd:nonNegativeInteger', + }, + 'units': 'as:units', + 'updated': { + '@id': 'as:updated', + '@type': 'xsd:dateTime', + }, + 'width': { + '@id': 'as:width', + '@type': 'xsd:nonNegativeInteger', + }, + 'describes': { + '@id': 'as:describes', + '@type': '@id', + }, + 'formerType': { + '@id': 'as:formerType', + '@type': '@id', + }, + 'deleted': { + '@id': 'as:deleted', + '@type': 'xsd:dateTime', + }, + 'inbox': { + '@id': 'ldp:inbox', + '@type': '@id', + }, + 'outbox': { + '@id': 'as:outbox', + '@type': '@id', + }, + 'following': { + '@id': 'as:following', + '@type': '@id', + }, + 'followers': { + '@id': 'as:followers', + '@type': '@id', + }, + 'streams': { + '@id': 'as:streams', + '@type': '@id', + }, + 'preferredUsername': 'as:preferredUsername', + 'endpoints': { + '@id': 'as:endpoints', + '@type': '@id', + }, + 'uploadMedia': { + '@id': 'as:uploadMedia', + '@type': '@id', + }, + 'proxyUrl': { + '@id': 'as:proxyUrl', + '@type': '@id', + }, + 'liked': { + '@id': 'as:liked', + '@type': '@id', + }, + 'oauthAuthorizationEndpoint': { + '@id': 'as:oauthAuthorizationEndpoint', + '@type': '@id', + }, + 'oauthTokenEndpoint': { + '@id': 'as:oauthTokenEndpoint', + '@type': '@id', + }, + 'provideClientKey': { + '@id': 'as:provideClientKey', + '@type': '@id', + }, + 'signClientKey': { + '@id': 'as:signClientKey', + '@type': '@id', + }, + 'sharedInbox': { + '@id': 'as:sharedInbox', + '@type': '@id', + }, + 'Public': { + '@id': 'as:Public', + '@type': '@id', + }, + 'source': 'as:source', + 'likes': { + '@id': 'as:likes', + '@type': '@id', + }, + 'shares': { + '@id': 'as:shares', + '@type': '@id', + }, + 'alsoKnownAs': { + '@id': 'as:alsoKnownAs', + '@type': '@id', + }, + }, +}; + +export const CONTEXTS: Record = { + 'https://w3id.org/identity/v1': id_v1, + 'https://w3id.org/security/v1': security_v1, + 'https://www.w3.org/ns/activitystreams': activitystreams, +}; diff --git a/packages/backend/src/core/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/core/remote/activitypub/misc/get-note-html.ts new file mode 100644 index 0000000000..af23a04a71 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/misc/get-note-html.ts @@ -0,0 +1,8 @@ +import * as mfm from 'mfm-js'; +import type { Note } from '@/models/entities/Note.js'; +import { toHtml } from '../../../../mfm/to-html.js'; + +export default function(note: Note) { + if (!note.text) return ''; + return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); +} diff --git a/packages/backend/src/core/remote/activitypub/models/ApImageService.ts b/packages/backend/src/core/remote/activitypub/models/ApImageService.ts new file mode 100644 index 0000000000..d4e01923a2 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/ApImageService.ts @@ -0,0 +1,90 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { MetaService } from '@/core/MetaService.js'; +import { truncate } from '@/misc/truncate.js'; +import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; +import { DriveService } from '@/core/DriveService.js'; +import type Logger from '@/logger.js'; +import { ApResolverService } from '../ApResolverService.js'; +import { ApLoggerService } from '../ApLoggerService.js'; + +@Injectable() +export class ApImageService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private metaService: MetaService, + private apResolverService: ApResolverService, + private driveService: DriveService, + private apLoggerService: ApLoggerService, + ) { + this.#logger = this.apLoggerService.logger; + } + + /** + * Imageを作成します。 + */ + public async createImage(actor: CacheableRemoteUser, value: any): Promise { + // 投稿者が凍結されていたらスキップ + if (actor.isSuspended) { + throw new Error('actor has been suspended'); + } + + const image = await this.apResolverService.createResolver().resolve(value) as any; + + if (image.url == null) { + throw new Error('invalid image: url not privided'); + } + + this.#logger.info(`Creating the Image: ${image.url}`); + + const instance = await this.metaService.fetch(); + + let file = await this.driveService.uploadFromUrl({ + url: image.url, + user: actor, + uri: image.url, + sensitive: image.sensitive, + isLink: !instance.cacheRemoteFiles, + comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), + }); + + if (file.isLink) { + // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 + // URLを更新する + if (file.url !== image.url) { + await this.driveFilesRepository.update({ id: file.id }, { + url: image.url, + uri: image.url, + }); + + file = await this.driveFilesRepository.findOneByOrFail({ id: file.id }); + } + } + + return file; + } + + /** + * Imageを解決します。 + * + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ + public async resolveImage(actor: CacheableRemoteUser, value: any): Promise { + // TODO + + // リモートサーバーからフェッチしてきて登録 + return await this.createImage(actor, value); + } +} diff --git a/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts b/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts new file mode 100644 index 0000000000..898da07a26 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import promiseLimit from 'promise-limit'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { toArray, unique } from '@/misc/prelude/array.js'; +import type { CacheableUser } from '@/models/entities/User.js'; +import { isMention } from '../type.js'; +import { ApResolverService } from '../ApResolverService.js'; +import { ApPersonService } from './ApPersonService.js'; +import type { IObject, IApMention } from '../type.js'; + +@Injectable() +export class ApMentionService { + constructor( + @Inject(DI.config) + private config: Config, + + private apResolverService: ApResolverService, + private apPersonService: ApPersonService, + ) { + } + + public async extractApMentions(tags: IObject | IObject[] | null | undefined) { + const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); + + const resolver = this.apResolverService.createResolver(); + + const limit = promiseLimit(2); + const mentionedUsers = (await Promise.all( + hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), + )).filter((x): x is CacheableUser => x != null); + + return mentionedUsers; + } + + public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { + if (tags == null) return []; + return toArray(tags).filter(isMention); + } +} diff --git a/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts b/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts new file mode 100644 index 0000000000..c74949c595 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts @@ -0,0 +1,403 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import promiseLimit from 'promise-limit'; +import { DI } from '@/di-symbols.js'; +import { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { MetaService } from '@/core/MetaService.js'; +import { AppLockService } from '@/core/AppLockService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import type Logger from '@/logger.js'; +import { IdService } from '@/core/IdService.js'; +import { PollService } from '@/core/PollService.js'; +import { StatusError } from '@/misc/status-error.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { MessagingService } from '@/core/MessagingService.js'; +import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { ApLoggerService } from '../ApLoggerService.js'; +import { ApMfmService } from '../ApMfmService.js'; +import { ApDbResolverService } from '../ApDbResolverService.js'; +import { ApResolverService } from '../ApResolverService.js'; +import { ApAudienceService } from '../ApAudienceService.js'; +import { ApPersonService } from './ApPersonService.js'; +import { extractApHashtags } from './tag.js'; +import { ApMentionService } from './ApMentionService.js'; +import { ApQuestionService } from './ApQuestionService.js'; +import { ApImageService } from './ApImageService.js'; +import type { Resolver } from '../ApResolverService.js'; +import type { IObject, IPost } from '../type.js'; + +@Injectable() +export class ApNoteService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + private idService: IdService, + private apMfmService: ApMfmService, + private apResolverService: ApResolverService, + + // 循環参照のため / for circular dependency + @Inject(forwardRef(() => ApPersonService)) + private apPersonService: ApPersonService, + + private utilityService: UtilityService, + private apAudienceService: ApAudienceService, + private apMentionService: ApMentionService, + private apImageService: ApImageService, + private apQuestionService: ApQuestionService, + private metaService: MetaService, + private messagingService: MessagingService, + private appLockService: AppLockService, + private pollService: PollService, + private noteCreateService: NoteCreateService, + private apDbResolverService: ApDbResolverService, + private apLoggerService: ApLoggerService, + ) { + this.#logger = this.apLoggerService.logger; + } + + public validateNote(object: any, uri: string) { + const expectHost = this.utilityService.extractDbHost(uri); + + if (object == null) { + return new Error('invalid Note: object is null'); + } + + if (!validPost.includes(getApType(object))) { + return new Error(`invalid Note: invalid object type ${getApType(object)}`); + } + + if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { + return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + } + + if (object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)) !== expectHost) { + return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.attributedTo)}`); + } + + return null; + } + + /** + * Noteをフェッチします。 + * + * Misskeyに対象のNoteが登録されていればそれを返します。 + */ + public async fetchNote(object: string | IObject): Promise { + return await this.apDbResolverService.getNoteFromApId(object); + } + + /** + * Noteを作成します。 + */ + public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const object: any = await resolver.resolve(value); + + const entryUri = getApId(value); + const err = this.validateNote(object, entryUri); + if (err) { + this.#logger.error(`${err.message}`, { + resolver: { + history: resolver.getHistory(), + }, + value: value, + object: object, + }); + throw new Error('invalid note'); + } + + const note: IPost = object; + + this.#logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); + + this.#logger.info(`Creating the Note: ${note.id}`); + + // 投稿者をフェッチ + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; + + // 投稿者が凍結されていたらスキップ + if (actor.isSuspended) { + throw new Error('actor has been suspended'); + } + + const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc); + let visibility = noteAudience.visibility; + const visibleUsers = noteAudience.visibleUsers; + + // Audience (to, cc) が指定されてなかった場合 + if (visibility === 'specified' && visibleUsers.length === 0) { + if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している + // こちらから匿名GET出来たものならばpublic + visibility = 'public'; + } + } + + let isMessaging = note._misskey_talk && visibility === 'specified'; + + const apMentions = await this.apMentionService.extractApMentions(note.tag); + const apHashtags = await extractApHashtags(note.tag); + + // 添付ファイル + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + // Noteがsensitiveなら添付もsensitiveにする + const limit = promiseLimit(2); + + note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; + const files = note.attachment + .map(attach => attach.sensitive = note.sensitive) + ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise))) + .filter(image => image != null) + : []; + + // リプライ + const reply: Note | null = note.inReplyTo + ? await this.resolveNote(note.inReplyTo, resolver).then(x => { + if (x == null) { + this.#logger.warn('Specified inReplyTo, but nout found'); + throw new Error('inReplyTo not found'); + } else { + return x; + } + }).catch(async err => { + // トークだったらinReplyToのエラーは無視 + const uri = getApId(note.inReplyTo); + if (uri.startsWith(this.config.url + '/')) { + const id = uri.split('/').pop(); + const talk = await this.messagingMessagesRepository.findOneBy({ id }); + if (talk) { + isMessaging = true; + return null; + } + } + + this.#logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + throw err; + }) + : null; + + // 引用 + let quote: Note | undefined | null; + + if (note._misskey_quote || note.quoteUrl) { + const tryResolveNote = async (uri: string): Promise<{ + status: 'ok'; + res: Note | null; + } | { + status: 'permerror' | 'temperror'; + }> => { + if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' }; + try { + const res = await this.resolveNote(uri); + if (res) { + return { + status: 'ok', + res, + }; + } else { + return { + status: 'permerror', + }; + } + } catch (e) { + return { + status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', + }; + } + }; + + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); + const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); + + quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); + if (!quote) { + if (results.some(x => x.status === 'temperror')) { + throw 'quote resolve failed'; + } + } + } + + const cw = note.summary === '' ? null : note.summary; + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + + // vote + if (reply && reply.hasPoll) { + const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); + + const tryCreateVote = async (name: string, index: number): Promise => { + if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { + this.#logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); + } else if (index >= 0) { + this.#logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); + await this.pollService.vote(actor, reply, index); + + // リモートフォロワーにUpdate配信 + this.pollService.deliverQuestionUpdate(reply.id); + } + return null; + }; + + if (note.name) { + return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); + } + } + + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { + this.#logger.info(`extractEmojis: ${e}`); + return [] as Emoji[]; + }); + + const apEmojis = emojis.map(emoji => emoji.name); + + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); + + if (isMessaging) { + for (const recipient of visibleUsers) { + await this.messagingService.createMessage(actor, recipient, undefined, text ?? undefined, (files && files.length > 0) ? files[0] : null, object.id); + return null; + } + } + + return await this.noteCreateService.create(actor, { + createdAt: note.published ? new Date(note.published) : null, + files, + reply, + renote: quote, + name: note.name, + cw, + text, + localOnly: false, + visibility, + visibleUsers, + apMentions, + apHashtags, + apEmojis, + poll, + uri: note.id, + url: getOneApHrefNullable(note.url), + }, silent); + } + + /** + * Noteを解決します。 + * + * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ + 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'); + + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) throw { statusCode: 451 }; + + const unlock = await this.appLockService.getApLock(uri); + + try { + //#region このサーバーに既に登録されていたらそれを返す + const exist = await this.fetchNote(uri); + + if (exist) { + return exist; + } + //#endregion + + if (uri.startsWith(this.config.url)) { + throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); + } + + // リモートサーバーからフェッチしてきて登録 + // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが + // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 + return await this.createNote(uri, resolver, true); + } finally { + unlock(); + } + } + + public async extractEmojis(tags: IObject | IObject[], host: string): Promise { + host = this.utilityService.toPuny(host); + + if (!tags) return []; + + const eomjiTags = toArray(tags).filter(isEmoji); + + return await Promise.all(eomjiTags.map(async tag => { + const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); + tag.icon = toSingle(tag.icon); + + const exists = await this.emojisRepository.findOneBy({ + host, + name, + }); + + if (exists) { + if ((tag.updated != null && exists.updatedAt == null) + || (tag.id != null && exists.uri == null) + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + || (tag.icon!.url !== exists.originalUrl) + ) { + await this.emojisRepository.update({ + host, + name, + }, { + uri: tag.id, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, + updatedAt: new Date(), + }); + + return await this.emojisRepository.findOneBy({ + host, + name, + }) as Emoji; + } + + return exists; + } + + this.#logger.info(`register emoji host=${host}, name=${name}`); + + return await this.emojisRepository.insert({ + id: this.idService.genId(), + host, + name, + uri: tag.id, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, + updatedAt: new Date(), + aliases: [], + } as Partial).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + })); + } +} diff --git a/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts b/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts new file mode 100644 index 0000000000..1ca6463484 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts @@ -0,0 +1,594 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import promiseLimit from 'promise-limit'; +import { DataSource } from 'typeorm'; +import { ModuleRef } from '@nestjs/core'; +import { DI } from '@/di-symbols.js'; +import { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { CacheableUser, IRemoteUser } from '@/models/entities/User.js'; +import { User } from '@/models/entities/User.js'; +import { truncate } from '@/misc/truncate.js'; +import type { UserCacheService } from '@/core/UserCacheService.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import type Logger from '@/logger.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { IdService } from '@/core/IdService.js'; +import type { MfmService } from '@/core/MfmService.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { toArray } from '@/misc/prelude/array.js'; +import type { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import type { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import type UsersChart from '@/core/chart/charts/users.js'; +import type InstanceChart from '@/core/chart/charts/instance.js'; +import type { HashtagService } from '@/core/HashtagService.js'; +import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { StatusError } from '@/misc/status-error.js'; +import type { UtilityService } from '@/core/UtilityService.js'; +import type { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; +import { extractApHashtags } from './tag.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { ApNoteService } from './ApNoteService.js'; +import type { ApMfmService } from '../ApMfmService.js'; +import type { ApResolverService, Resolver } from '../ApResolverService.js'; +import type { ApLoggerService } from '../ApLoggerService.js'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import type { ApImageService } from './ApImageService.js'; +import type { IActor, IObject, IApPropertyValue } from '../type.js'; + +const nameLength = 128; +const summaryLength = 2048; + +const services: { + [x: string]: (id: string, username: string) => any +} = { + 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), + 'misskey:authentication:github': (id, login) => ({ id, login }), + 'misskey:authentication:discord': (id, name) => $discord(id, name), +}; + +const $discord = (id: string, name: string) => { + if (typeof name !== 'string') { + name = 'unknown#0000'; + } + const [username, discriminator] = name.split('#'); + return { id, username, discriminator }; +}; + +function addService(target: { [x: string]: any }, source: IApPropertyValue) { + const service = services[source.name]; + + if (typeof source.value !== 'string') { + source.value = 'unknown'; + } + + const [id, username] = source.value.split('@'); + + if (service) { + target[source.name.split(':')[2]] = service(id, username); + } +} + +@Injectable() +export class ApPersonService implements OnModuleInit { + private utilityService: UtilityService; + private userEntityService: UserEntityService; + private idService: IdService; + private globalEventService: GlobalEventService; + private federatedInstanceService: FederatedInstanceService; + private fetchInstanceMetadataService: FetchInstanceMetadataService; + private userCacheService: UserCacheService; + private apResolverService: ApResolverService; + private apNoteService: ApNoteService; + private apImageService: ApImageService; + private apMfmService: ApMfmService; + private mfmService: MfmService; + private hashtagService: HashtagService; + private usersChart: UsersChart; + private instanceChart: InstanceChart; + private apLoggerService: ApLoggerService; + #logger: Logger; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.userPublickeysRepository) + private userPublickeysRepository: UserPublickeysRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + //private utilityService: UtilityService, + //private userEntityService: UserEntityService, + //private idService: IdService, + //private globalEventService: GlobalEventService, + //private federatedInstanceService: FederatedInstanceService, + //private fetchInstanceMetadataService: FetchInstanceMetadataService, + //private userCacheService: UserCacheService, + //private apResolverService: ApResolverService, + //private apNoteService: ApNoteService, + //private apImageService: ApImageService, + //private apMfmService: ApMfmService, + //private mfmService: MfmService, + //private hashtagService: HashtagService, + //private usersChart: UsersChart, + //private instanceChart: InstanceChart, + //private apLoggerService: ApLoggerService, + ) { + } + + onModuleInit() { + this.utilityService = this.moduleRef.get('UtilityService'); + this.userEntityService = this.moduleRef.get('UserEntityService'); + this.idService = this.moduleRef.get('IdService'); + this.globalEventService = this.moduleRef.get('GlobalEventService'); + this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); + this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); + this.userCacheService = this.moduleRef.get('UserCacheService'); + this.apResolverService = this.moduleRef.get('ApResolverService'); + this.apNoteService = this.moduleRef.get('ApNoteService'); + this.apImageService = this.moduleRef.get('ApImageService'); + this.apMfmService = this.moduleRef.get('ApMfmService'); + this.mfmService = this.moduleRef.get('MfmService'); + this.hashtagService = this.moduleRef.get('HashtagService'); + this.usersChart = this.moduleRef.get('UsersChart'); + this.instanceChart = this.moduleRef.get('InstanceChart'); + this.apLoggerService = this.moduleRef.get('ApLoggerService'); + this.#logger = this.apLoggerService.logger; + } + + /** + * Validate and convert to actor object + * @param x Fetched object + * @param uri Fetch target URI + */ + #validateActor(x: IObject, uri: string): IActor { + const expectHost = this.utilityService.toPuny(new URL(uri).hostname); + + if (x == null) { + throw new Error('invalid Actor: object is null'); + } + + if (!isActor(x)) { + throw new Error(`invalid Actor type '${x.type}'`); + } + + if (!(typeof x.id === 'string' && x.id.length > 0)) { + throw new Error('invalid Actor: wrong id'); + } + + if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { + throw new Error('invalid Actor: wrong inbox'); + } + + if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { + throw new Error('invalid Actor: wrong username'); + } + + // These fields are only informational, and some AP software allows these + // fields to be very long. If they are too long, we cut them off. This way + // we can at least see these users and their activities. + if (x.name) { + if (!(typeof x.name === 'string' && x.name.length > 0)) { + throw new Error('invalid Actor: wrong name'); + } + x.name = truncate(x.name, nameLength); + } + if (x.summary) { + if (!(typeof x.summary === 'string' && x.summary.length > 0)) { + throw new Error('invalid Actor: wrong summary'); + } + x.summary = truncate(x.summary, summaryLength); + } + + const idHost = this.utilityService.toPuny(new URL(x.id!).hostname); + if (idHost !== expectHost) { + throw new Error('invalid Actor: id has different host'); + } + + if (x.publicKey) { + if (typeof x.publicKey.id !== 'string') { + throw new Error('invalid Actor: publicKey.id is not a string'); + } + + const publicKeyIdHost = this.utilityService.toPuny(new URL(x.publicKey.id).hostname); + if (publicKeyIdHost !== expectHost) { + throw new Error('invalid Actor: publicKey.id has different host'); + } + } + + return x; + } + + /** + * Personをフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返します。 + */ + public async fetchPerson(uri: string, resolver?: Resolver): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + + const cached = this.userCacheService.uriPersonCache.get(uri); + if (cached) return cached; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(this.config.url + '/')) { + const id = uri.split('/').pop(); + const u = await this.usersRepository.findOneBy({ id }); + if (u) this.userCacheService.uriPersonCache.set(uri, u); + return u; + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await this.usersRepository.findOneBy({ uri }); + + if (exist) { + this.userCacheService.uriPersonCache.set(uri, exist); + return exist; + } + //#endregion + + return null; + } + + /** + * Personを作成します。 + */ + public async createPerson(uri: string, resolver?: Resolver): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + + if (uri.startsWith(this.config.url)) { + throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + } + + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(uri) as any; + + const person = this.#validateActor(object, uri); + + this.#logger.info(`Creating the Person: ${person.id}`); + + const host = this.utilityService.toPuny(new URL(object.id).hostname); + + const { fields } = this.analyzeAttachments(person.attachment ?? []); + + const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); + + const isBot = getApType(object) === 'Service'; + + const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); + + // Create user + let user: IRemoteUser; + try { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + user = await transactionalEntityManager.save(new User({ + id: this.idService.genId(), + avatarId: null, + bannerId: null, + createdAt: new Date(), + lastFetchedAt: new Date(), + name: truncate(person.name, nameLength), + isLocked: !!person.manuallyApprovesFollowers, + isExplorable: !!person.discoverable, + username: person.preferredUsername, + usernameLower: person.preferredUsername!.toLowerCase(), + host, + inbox: person.inbox, + sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + followersUri: person.followers ? getApId(person.followers) : undefined, + featured: person.featured ? getApId(person.featured) : undefined, + uri: person.id, + tags, + isBot, + isCat: (person as any).isCat === true, + showTimelineReplies: false, + })) as IRemoteUser; + + await transactionalEntityManager.save(new UserProfile({ + userId: user.id, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, + url: getOneApHrefNullable(person.url), + fields, + birthday: bday ? bday[0] : null, + location: person['vcard:Address'] ?? null, + userHost: host, + })); + + if (person.publicKey) { + await transactionalEntityManager.save(new UserPublickey({ + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem, + })); + } + }); + } catch (e) { + // duplicate key error + if (isDuplicateKeyValueError(e)) { + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + const u = await this.usersRepository.findOneBy({ + uri: person.id, + }); + + if (u) { + user = u as IRemoteUser; + } else { + throw new Error('already registered'); + } + } else { + this.#logger.error(e instanceof Error ? e : new Error(e as string)); + throw e; + } + } + + // Register host + this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { + this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); + this.instanceChart.newUser(i.host); + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + }); + + this.usersChart.update(user!, true); + + // ハッシュタグ更新 + this.hashtagService.updateUsertags(user!, tags); + + //#region アバターとヘッダー画像をフェッチ + const [avatar, banner] = await Promise.all([ + person.icon, + person.image, + ].map(img => + img == null + ? Promise.resolve(null) + : this.apImageService.resolveImage(user!, img).catch(() => null), + )); + + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + + await this.usersRepository.update(user!.id, { + avatarId, + bannerId, + }); + + user!.avatarId = avatarId; + user!.bannerId = bannerId; + //#endregion + + //#region カスタム絵文字取得 + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { + this.#logger.info(`extractEmojis: ${err}`); + return [] as Emoji[]; + }); + + const emojiNames = emojis.map(emoji => emoji.name); + + await this.usersRepository.update(user!.id, { + emojis: emojiNames, + }); + //#endregion + + await this.updateFeatured(user!.id).catch(err => this.#logger.error(err)); + + return user!; + } + + /** + * Personの情報を更新します。 + * Misskeyに対象のPersonが登録されていなければ無視します。 + * @param uri URI of Person + * @param resolver Resolver + * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) + */ + public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + + // URIがこのサーバーを指しているならスキップ + if (uri.startsWith(this.config.url + '/')) { + return; + } + + //#region このサーバーに既に登録されているか + const exist = await this.usersRepository.findOneBy({ uri }) as IRemoteUser; + + if (exist == null) { + return; + } + //#endregion + + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const object = hint ?? await resolver.resolve(uri); + + const person = this.#validateActor(object, uri); + + this.#logger.info(`Updating the Person: ${person.id}`); + + // アバターとヘッダー画像をフェッチ + const [avatar, banner] = await Promise.all([ + person.icon, + person.image, + ].map(img => + img == null + ? Promise.resolve(null) + : this.apImageService.resolveImage(exist, img).catch(() => null), + )); + + // カスタム絵文字取得 + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { + this.#logger.info(`extractEmojis: ${e}`); + return [] as Emoji[]; + }); + + const emojiNames = emojis.map(emoji => emoji.name); + + const { fields } = this.analyzeAttachments(person.attachment ?? []); + + const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); + + const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); + + const updates = { + lastFetchedAt: new Date(), + inbox: person.inbox, + sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + followersUri: person.followers ? getApId(person.followers) : undefined, + featured: person.featured, + emojis: emojiNames, + name: truncate(person.name, nameLength), + tags, + isBot: getApType(object) === 'Service', + isCat: (person as any).isCat === true, + isLocked: !!person.manuallyApprovesFollowers, + isExplorable: !!person.discoverable, + } as Partial; + + if (avatar) { + updates.avatarId = avatar.id; + } + + if (banner) { + updates.bannerId = banner.id; + } + + // Update user + await this.usersRepository.update(exist.id, updates); + + if (person.publicKey) { + await this.userPublickeysRepository.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem, + }); + } + + await this.userProfilesRepository.update({ userId: exist.id }, { + url: getOneApHrefNullable(person.url), + fields, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, + birthday: bday ? bday[0] : null, + location: person['vcard:Address'] ?? null, + }); + + this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id }); + + // ハッシュタグ更新 + this.hashtagService.updateUsertags(exist, tags); + + // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする + await this.followingsRepository.update({ + followerId: exist.id, + }, { + followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + }); + + await this.updateFeatured(exist.id).catch(err => this.#logger.error(err)); + } + + /** + * Personを解決します。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ + public async resolvePerson(uri: string, resolver?: Resolver): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await this.fetchPerson(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + if (resolver == null) resolver = this.apResolverService.createResolver(); + return await this.createPerson(uri, resolver); + } + + public analyzeAttachments(attachments: IObject | IObject[] | undefined) { + const fields: { + name: string, + value: string + }[] = []; + const services: { [x: string]: any } = {}; + + if (Array.isArray(attachments)) { + for (const attachment of attachments.filter(isPropertyValue)) { + if (isPropertyValue(attachment.identifier)) { + addService(services, attachment.identifier); + } else { + fields.push({ + name: attachment.name, + value: this.mfmService.fromHtml(attachment.value), + }); + } + } + } + + return { fields, services }; + } + + public async updateFeatured(userId: User['id']) { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); + if (!this.userEntityService.isRemoteUser(user)) return; + if (!user.featured) return; + + this.#logger.info(`Updating the featured: ${user.uri}`); + + const resolver = this.apResolverService.createResolver(); + + // Resolve to (Ordered)Collection Object + const collection = await resolver.resolveCollection(user.featured); + if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); + + // Resolve to Object(may be Note) arrays + const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; + const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x))); + + // Resolve and regist Notes + const limit = promiseLimit(2); + const featuredNotes = await Promise.all(items + .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも + .slice(0, 5) + .map(item => limit(() => this.apNoteService.resolveNote(item, resolver)))); + + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); + + // とりあえずidを別の時間で生成して順番を維持 + let td = 0; + for (const note of featuredNotes.filter(note => note != null)) { + td -= 1000; + transactionalEntityManager.insert(UserNotePining, { + id: this.idService.genId(new Date(Date.now() + td)), + createdAt: new Date(), + userId: user.id, + noteId: note!.id, + }); + } + }); + } +} diff --git a/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts new file mode 100644 index 0000000000..97f26d1681 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository, PollsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type { IPoll } from '@/models/entities/Poll.js'; +import type Logger from '@/logger.js'; +import { isQuestion } from '../type.js'; +import { ApLoggerService } from '../ApLoggerService.js'; +import { ApResolverService } from '../ApResolverService.js'; +import type { Resolver } from '../ApResolverService.js'; +import type { IObject, IQuestion } from '../type.js'; + +@Injectable() +export class ApQuestionService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + private apResolverService: ApResolverService, + private apLoggerService: ApLoggerService, + ) { + this.#logger = this.apLoggerService.logger; + } + + public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const question = await resolver.resolve(source); + + if (!isQuestion(question)) { + throw new Error('invalid type'); + } + + const multiple = !question.oneOf; + const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; + + if (multiple && !question.anyOf) { + throw new Error('invalid question'); + } + + const choices = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.name!); + + const votes = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); + + return { + choices, + votes, + multiple, + expiresAt, + }; + } + + /** + * Update votes of Question + * @param uri URI of AP Question object + * @returns true if updated + */ + public async updateQuestion(value: any) { + const uri = typeof value === 'string' ? value : value.id; + + // URIがこのサーバーを指しているならスキップ + if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + + //#region このサーバーに既に登録されているか + const note = await this.notesRepository.findOneBy({ uri }); + if (note == null) throw new Error('Question is not registed'); + + const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); + if (poll == null) throw new Error('Question is not registed'); + //#endregion + + // resolve new Question object + const resolver = this.apResolverService.createResolver(); + const question = await resolver.resolve(value) as IQuestion; + this.#logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); + + if (question.type !== 'Question') throw new Error('object is not a Question'); + + const apChoices = question.oneOf ?? question.anyOf; + + let changed = false; + + for (const choice of poll.choices) { + const oldCount = poll.votes[poll.choices.indexOf(choice)]; + const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; + + if (oldCount !== newCount) { + changed = true; + poll.votes[poll.choices.indexOf(choice)] = newCount; + } + } + + await this.pollsRepository.update({ noteId: note.id }, { + votes: poll.votes, + }); + + return changed; + } +} diff --git a/packages/backend/src/core/remote/activitypub/models/icon.ts b/packages/backend/src/core/remote/activitypub/models/icon.ts new file mode 100644 index 0000000000..50794a937d --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/icon.ts @@ -0,0 +1,5 @@ +export type IIcon = { + type: string; + mediaType?: string; + url?: string; +}; diff --git a/packages/backend/src/core/remote/activitypub/models/identifier.ts b/packages/backend/src/core/remote/activitypub/models/identifier.ts new file mode 100644 index 0000000000..f6c3bb8c88 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/identifier.ts @@ -0,0 +1,5 @@ +export type IIdentifier = { + type: string; + name: string; + value: string; +}; diff --git a/packages/backend/src/core/remote/activitypub/models/tag.ts b/packages/backend/src/core/remote/activitypub/models/tag.ts new file mode 100644 index 0000000000..803846a0b0 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/models/tag.ts @@ -0,0 +1,19 @@ +import { toArray } from '@/misc/prelude/array.js'; +import { isHashtag } from '../type.js'; +import type { IObject, IApHashtag } from '../type.js'; + +export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { + if (tags == null) return []; + + const hashtags = extractApHashtagObjects(tags); + + return hashtags.map(tag => { + const m = tag.name.match(/^#(.+)/); + return m ? m[1] : null; + }).filter((x): x is string => x != null); +} + +export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { + if (tags == null) return []; + return toArray(tags).filter(isHashtag); +} diff --git a/packages/backend/src/core/remote/activitypub/type.ts b/packages/backend/src/core/remote/activitypub/type.ts new file mode 100644 index 0000000000..de7eb0ed83 --- /dev/null +++ b/packages/backend/src/core/remote/activitypub/type.ts @@ -0,0 +1,295 @@ +export type obj = { [x: string]: any }; +export type ApObject = IObject | string | (IObject | string)[]; + +export interface IObject { + '@context': string | string[] | obj | obj[]; + type: string | string[]; + id?: string; + summary?: string; + published?: string; + cc?: ApObject; + to?: ApObject; + attributedTo: ApObject; + attachment?: any[]; + inReplyTo?: any; + replies?: ICollection; + content?: string; + name?: string; + startTime?: Date; + endTime?: Date; + icon?: any; + image?: any; + url?: ApObject; + href?: string; + tag?: IObject | IObject[]; + sensitive?: boolean; +} + +/** + * Get array of ActivityStreams Objects id + */ +export function getApIds(value: ApObject | undefined): string[] { + if (value == null) return []; + const array = Array.isArray(value) ? value : [value]; + return array.map(x => getApId(x)); +} + +/** + * Get first ActivityStreams Object id + */ +export function getOneApId(value: ApObject): string { + const firstOne = Array.isArray(value) ? value[0] : value; + return getApId(firstOne); +} + +/** + * Get ActivityStreams Object id + */ +export function getApId(value: string | IObject): string { + if (typeof value === 'string') return value; + if (typeof value.id === 'string') return value.id; + throw new Error('cannot detemine id'); +} + +/** + * Get ActivityStreams Object type + */ +export function getApType(value: IObject): string { + if (typeof value.type === 'string') return value.type; + if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; + throw new Error('cannot detect type'); +} + +export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { + const firstOne = Array.isArray(value) ? value[0] : value; + return getApHrefNullable(firstOne); +} + +export function getApHrefNullable(value: string | IObject | undefined): string | undefined { + if (typeof value === 'string') return value; + if (typeof value?.href === 'string') return value.href; + return undefined; +} + +export interface IActivity extends IObject { + //type: 'Activity'; + actor: IObject | string; + object: IObject | string; + target?: IObject | string; + /** LD-Signature */ + signature?: { + type: string; + created: Date; + creator: string; + domain?: string; + nonce?: string; + signatureValue: string; + }; +} + +export interface ICollection extends IObject { + type: 'Collection'; + totalItems: number; + items: ApObject; +} + +export interface IOrderedCollection extends IObject { + type: 'OrderedCollection'; + totalItems: number; + orderedItems: ApObject; +} + +export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; + +export const isPost = (object: IObject): object is IPost => + validPost.includes(getApType(object)); + +export interface IPost extends IObject { + type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; + source?: { + content: string; + mediaType: string; + }; + _misskey_quote?: string; + quoteUrl?: string; + _misskey_talk: boolean; +} + +export interface IQuestion extends IObject { + type: 'Note' | 'Question'; + source?: { + content: string; + mediaType: string; + }; + _misskey_quote?: string; + quoteUrl?: string; + oneOf?: IQuestionChoice[]; + anyOf?: IQuestionChoice[]; + endTime?: Date; + closed?: Date; +} + +export const isQuestion = (object: IObject): object is IQuestion => + getApType(object) === 'Note' || getApType(object) === 'Question'; + +interface IQuestionChoice { + name?: string; + replies?: ICollection; + _misskey_votes?: number; +} +export interface ITombstone extends IObject { + type: 'Tombstone'; + formerType?: string; + deleted?: Date; +} + +export const isTombstone = (object: IObject): object is ITombstone => + getApType(object) === 'Tombstone'; + +export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; + +export const isActor = (object: IObject): object is IActor => + validActor.includes(getApType(object)); + +export interface IActor extends IObject { + type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; + name?: string; + preferredUsername?: string; + manuallyApprovesFollowers?: boolean; + discoverable?: boolean; + inbox: string; + sharedInbox?: string; // 後方互換性のため + publicKey?: { + id: string; + publicKeyPem: string; + }; + followers?: string | ICollection | IOrderedCollection; + following?: string | ICollection | IOrderedCollection; + featured?: string | IOrderedCollection; + outbox: string | IOrderedCollection; + endpoints?: { + sharedInbox?: string; + }; + 'vcard:bday'?: string; + 'vcard:Address'?: string; +} + +export const isCollection = (object: IObject): object is ICollection => + getApType(object) === 'Collection'; + +export const isOrderedCollection = (object: IObject): object is IOrderedCollection => + getApType(object) === 'OrderedCollection'; + +export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => + isCollection(object) || isOrderedCollection(object); + +export interface IApPropertyValue extends IObject { + type: 'PropertyValue'; + identifier: IApPropertyValue; + name: string; + value: string; +} + +export const isPropertyValue = (object: IObject): object is IApPropertyValue => + object && + getApType(object) === 'PropertyValue' && + typeof object.name === 'string' && + typeof (object as any).value === 'string'; + +export interface IApMention extends IObject { + type: 'Mention'; + href: string; +} + +export const isMention = (object: IObject): object is IApMention => + getApType(object) === 'Mention' && + typeof object.href === 'string'; + +export interface IApHashtag extends IObject { + type: 'Hashtag'; + name: string; +} + +export const isHashtag = (object: IObject): object is IApHashtag => + getApType(object) === 'Hashtag' && + typeof object.name === 'string'; + +export interface IApEmoji extends IObject { + type: 'Emoji'; + updated: Date; +} + +export const isEmoji = (object: IObject): object is IApEmoji => + getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; + +export interface ICreate extends IActivity { + type: 'Create'; +} + +export interface IDelete extends IActivity { + type: 'Delete'; +} + +export interface IUpdate extends IActivity { + type: 'Update'; +} + +export interface IRead extends IActivity { + type: 'Read'; +} + +export interface IUndo extends IActivity { + type: 'Undo'; +} + +export interface IFollow extends IActivity { + type: 'Follow'; +} + +export interface IAccept extends IActivity { + type: 'Accept'; +} + +export interface IReject extends IActivity { + type: 'Reject'; +} + +export interface IAdd extends IActivity { + type: 'Add'; +} + +export interface IRemove extends IActivity { + type: 'Remove'; +} + +export interface ILike extends IActivity { + type: 'Like' | 'EmojiReaction' | 'EmojiReact'; + _misskey_reaction?: string; +} + +export interface IAnnounce extends IActivity { + type: 'Announce'; +} + +export interface IBlock extends IActivity { + type: 'Block'; +} + +export interface IFlag extends IActivity { + type: 'Flag'; +} + +export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; +export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; +export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; +export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; +export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; +export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; +export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; +export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; +export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; +export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; +export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; +export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; +export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; +export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/src/daemons/DaemonModule.ts b/packages/backend/src/daemons/DaemonModule.ts new file mode 100644 index 0000000000..683f9cbfe3 --- /dev/null +++ b/packages/backend/src/daemons/DaemonModule.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { CoreModule } from '@/core/CoreModule.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { JanitorService } from './JanitorService.js'; +import { QueueStatsService } from './QueueStatsService.js'; +import { ServerStatsService } from './ServerStatsService.js'; + +@Module({ + imports: [ + GlobalModule, + CoreModule, + ], + providers: [ + JanitorService, + QueueStatsService, + ServerStatsService, + ], + exports: [ + JanitorService, + QueueStatsService, + ServerStatsService, + ], +}) +export class DaemonModule {} diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts new file mode 100644 index 0000000000..dacbb8cca6 --- /dev/null +++ b/packages/backend/src/daemons/JanitorService.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { LessThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { AttestationChallengesRepository } from '@/models/index.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +const interval = 30 * 60 * 1000; + +@Injectable() +export class JanitorService implements OnApplicationShutdown { + #intervalId: NodeJS.Timer; + + constructor( + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, + ) { + } + + /** + * Clean up database occasionally + */ + public start(): void { + const tick = async () => { + await this.attestationChallengesRepository.delete({ + createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), + }); + }; + + tick(); + + this.#intervalId = setInterval(tick, interval); + } + + public onApplicationShutdown(signal?: string | undefined) { + clearInterval(this.#intervalId); + } +} diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts new file mode 100644 index 0000000000..f05d3ce33f --- /dev/null +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -0,0 +1,77 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Xev from 'xev'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +const ev = new Xev(); + +const interval = 10000; + +@Injectable() +export class QueueStatsService implements OnApplicationShutdown { + #intervalId: NodeJS.Timer; + + constructor( + private queueService: QueueService, + ) { + } + + /** + * Report queue stats regularly + */ + public start(): void { + const log = [] as any[]; + + ev.on('requestQueueStatsLog', x => { + ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); + }); + + let activeDeliverJobs = 0; + let activeInboxJobs = 0; + + this.queueService.deliverQueue.on('global:active', () => { + activeDeliverJobs++; + }); + + this.queueService.inboxQueue.on('global:active', () => { + activeInboxJobs++; + }); + + const tick = async () => { + const deliverJobCounts = await this.queueService.deliverQueue.getJobCounts(); + const inboxJobCounts = await this.queueService.inboxQueue.getJobCounts(); + + const stats = { + deliver: { + activeSincePrevTick: activeDeliverJobs, + active: deliverJobCounts.active, + waiting: deliverJobCounts.waiting, + delayed: deliverJobCounts.delayed, + }, + inbox: { + activeSincePrevTick: activeInboxJobs, + active: inboxJobCounts.active, + waiting: inboxJobCounts.waiting, + delayed: inboxJobCounts.delayed, + }, + }; + + ev.emit('queueStats', stats); + + log.unshift(stats); + if (log.length > 200) log.pop(); + + activeDeliverJobs = 0; + activeInboxJobs = 0; + }; + + tick(); + + this.#intervalId = setInterval(tick, interval); + } + + public onApplicationShutdown(signal?: string | undefined) { + clearInterval(this.#intervalId); + } +} diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts new file mode 100644 index 0000000000..3a6d408431 --- /dev/null +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -0,0 +1,95 @@ +import { Inject, Injectable } from '@nestjs/common'; +import si from 'systeminformation'; +import Xev from 'xev'; +import * as osUtils from 'os-utils'; +import { DI } from '@/di-symbols.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +const ev = new Xev(); + +const interval = 2000; + +const roundCpu = (num: number) => Math.round(num * 1000) / 1000; +const round = (num: number) => Math.round(num * 10) / 10; + +@Injectable() +export class ServerStatsService implements OnApplicationShutdown { + #intervalId: NodeJS.Timer; + + constructor( + ) { + } + + /** + * Report server stats regularly + */ + public start(): void { + const log = [] as any[]; + + ev.on('requestServerStatsLog', x => { + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); + }); + + const tick = async () => { + const cpu = await cpuUsage(); + const memStats = await mem(); + const netStats = await net(); + const fsStats = await fs(); + + const stats = { + cpu: roundCpu(cpu), + mem: { + used: round(memStats.used - memStats.buffers - memStats.cached), + active: round(memStats.active), + }, + net: { + rx: round(Math.max(0, netStats.rx_sec)), + tx: round(Math.max(0, netStats.tx_sec)), + }, + fs: { + r: round(Math.max(0, fsStats.rIO_sec ?? 0)), + w: round(Math.max(0, fsStats.wIO_sec ?? 0)), + }, + }; + ev.emit('serverStats', stats); + log.unshift(stats); + if (log.length > 200) log.pop(); + }; + + tick(); + + this.#intervalId = setInterval(tick, interval); + } + + public onApplicationShutdown(signal?: string | undefined) { + clearInterval(this.#intervalId); + } +} + +// CPU STAT +function cpuUsage(): Promise { + return new Promise((res, rej) => { + osUtils.cpuUsage((cpuUsage) => { + res(cpuUsage); + }); + }); +} + +// MEMORY STAT +async function mem() { + const data = await si.mem(); + return data; +} + +// NETWORK STAT +async function net() { + const iface = await si.networkInterfaceDefault(); + const data = await si.networkStats(iface); + return data[0]; +} + +// FS STAT +async function fs() { + const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); + return data ?? { rIO_sec: 0, wIO_sec: 0 }; +} diff --git a/packages/backend/src/daemons/janitor.ts b/packages/backend/src/daemons/janitor.ts deleted file mode 100644 index f2a1bfcc2f..0000000000 --- a/packages/backend/src/daemons/janitor.ts +++ /dev/null @@ -1,20 +0,0 @@ -// TODO: 消したい - -const interval = 30 * 60 * 1000; -import { AttestationChallenges } from '@/models/index.js'; -import { LessThan } from 'typeorm'; - -/** - * Clean up database occasionally - */ -export default function() { - async function tick() { - await AttestationChallenges.delete({ - createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), - }); - } - - tick(); - - setInterval(tick, interval); -} diff --git a/packages/backend/src/daemons/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts deleted file mode 100644 index 1535abc6af..0000000000 --- a/packages/backend/src/daemons/queue-stats.ts +++ /dev/null @@ -1,60 +0,0 @@ -import Xev from 'xev'; -import { deliverQueue, inboxQueue } from '../queue/queues.js'; - -const ev = new Xev(); - -const interval = 10000; - -/** - * Report queue stats regularly - */ -export default function() { - const log = [] as any[]; - - ev.on('requestQueueStatsLog', x => { - ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50)); - }); - - let activeDeliverJobs = 0; - let activeInboxJobs = 0; - - deliverQueue.on('global:active', () => { - activeDeliverJobs++; - }); - - inboxQueue.on('global:active', () => { - activeInboxJobs++; - }); - - async function tick() { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - - const stats = { - deliver: { - activeSincePrevTick: activeDeliverJobs, - active: deliverJobCounts.active, - waiting: deliverJobCounts.waiting, - delayed: deliverJobCounts.delayed, - }, - inbox: { - activeSincePrevTick: activeInboxJobs, - active: inboxJobCounts.active, - waiting: inboxJobCounts.waiting, - delayed: inboxJobCounts.delayed, - }, - }; - - ev.emit('queueStats', stats); - - log.unshift(stats); - if (log.length > 200) log.pop(); - - activeDeliverJobs = 0; - activeInboxJobs = 0; - } - - tick(); - - setInterval(tick, interval); -} diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts deleted file mode 100644 index faf4e6e4a4..0000000000 --- a/packages/backend/src/daemons/server-stats.ts +++ /dev/null @@ -1,79 +0,0 @@ -import si from 'systeminformation'; -import Xev from 'xev'; -import * as osUtils from 'os-utils'; - -const ev = new Xev(); - -const interval = 2000; - -const roundCpu = (num: number) => Math.round(num * 1000) / 1000; -const round = (num: number) => Math.round(num * 10) / 10; - -/** - * Report server stats regularly - */ -export default function() { - const log = [] as any[]; - - ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); - }); - - async function tick() { - const cpu = await cpuUsage(); - const memStats = await mem(); - const netStats = await net(); - const fsStats = await fs(); - - const stats = { - cpu: roundCpu(cpu), - mem: { - used: round(memStats.used - memStats.buffers - memStats.cached), - active: round(memStats.active), - }, - net: { - rx: round(Math.max(0, netStats.rx_sec)), - tx: round(Math.max(0, netStats.tx_sec)), - }, - fs: { - r: round(Math.max(0, fsStats.rIO_sec ?? 0)), - w: round(Math.max(0, fsStats.wIO_sec ?? 0)), - }, - }; - ev.emit('serverStats', stats); - log.unshift(stats); - if (log.length > 200) log.pop(); - } - - tick(); - - setInterval(tick, interval); -} - -// CPU STAT -function cpuUsage(): Promise { - return new Promise((res, rej) => { - osUtils.cpuUsage((cpuUsage) => { - res(cpuUsage); - }); - }); -} - -// MEMORY STAT -async function mem() { - const data = await si.mem(); - return data; -} - -// NETWORK STAT -async function net() { - const iface = await si.networkInterfaceDefault(); - const data = await si.networkStats(iface); - return data[0]; -} - -// FS STAT -async function fs() { - const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); - return data || { rIO_sec: 0, wIO_sec: 0 }; -} diff --git a/packages/backend/src/db/elasticsearch.ts b/packages/backend/src/db/elasticsearch.ts deleted file mode 100644 index d98c5d180b..0000000000 --- a/packages/backend/src/db/elasticsearch.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as elasticsearch from '@elastic/elasticsearch'; -import config from '@/config/index.js'; - -const index = { - settings: { - analysis: { - analyzer: { - ngram: { - tokenizer: 'ngram', - }, - }, - }, - }, - mappings: { - properties: { - text: { - type: 'text', - index: true, - analyzer: 'ngram', - }, - userId: { - type: 'keyword', - index: true, - }, - userHost: { - type: 'keyword', - index: true, - }, - }, - }, -}; - -// Init ElasticSearch connection -const client = config.elasticsearch ? new elasticsearch.Client({ - node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`, - auth: (config.elasticsearch.user && config.elasticsearch.pass) ? { - username: config.elasticsearch.user, - password: config.elasticsearch.pass, - } : undefined, - pingTimeout: 30000, -}) : null; - -if (client) { - client.indices.exists({ - index: config.elasticsearch.index || 'misskey_note', - }).then(exist => { - if (!exist.body) { - client.indices.create({ - index: config.elasticsearch.index || 'misskey_note', - body: index, - }); - } - }); -} - -export default client; diff --git a/packages/backend/src/db/logger.ts b/packages/backend/src/db/logger.ts deleted file mode 100644 index 22f4c6b1b0..0000000000 --- a/packages/backend/src/db/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger.js'; - -export const dbLogger = new Logger('db'); diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts deleted file mode 100644 index 94d55e4310..0000000000 --- a/packages/backend/src/db/postgre.ts +++ /dev/null @@ -1,256 +0,0 @@ -// https://github.com/typeorm/typeorm/issues/2400 -import pg from 'pg'; -pg.types.setTypeParser(20, Number); - -import { Logger, DataSource } from 'typeorm'; -import * as highlight from 'cli-highlight'; -import config from '@/config/index.js'; - -import { User } from '@/models/entities/user.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { App } from '@/models/entities/app.js'; -import { PollVote } from '@/models/entities/poll-vote.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { NoteWatching } from '@/models/entities/note-watching.js'; -import { NoteThreadMuting } from '@/models/entities/note-thread-muting.js'; -import { NoteUnread } from '@/models/entities/note-unread.js'; -import { Notification } from '@/models/entities/notification.js'; -import { Meta } from '@/models/entities/meta.js'; -import { Following } from '@/models/entities/following.js'; -import { Instance } from '@/models/entities/instance.js'; -import { Muting } from '@/models/entities/muting.js'; -import { SwSubscription } from '@/models/entities/sw-subscription.js'; -import { Blocking } from '@/models/entities/blocking.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserListJoining } from '@/models/entities/user-list-joining.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; -import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; -import { Hashtag } from '@/models/entities/hashtag.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; -import { RegistrationTicket } from '@/models/entities/registration-tickets.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { Signin } from '@/models/entities/signin.js'; -import { AuthSession } from '@/models/entities/auth-session.js'; -import { FollowRequest } from '@/models/entities/follow-request.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { UserNotePining } from '@/models/entities/user-note-pining.js'; -import { Poll } from '@/models/entities/poll.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { UserPublickey } from '@/models/entities/user-publickey.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { UserSecurityKey } from '@/models/entities/user-security-key.js'; -import { AttestationChallenge } from '@/models/entities/attestation-challenge.js'; -import { Page } from '@/models/entities/page.js'; -import { PageLike } from '@/models/entities/page-like.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { GalleryLike } from '@/models/entities/gallery-like.js'; -import { ModerationLog } from '@/models/entities/moderation-log.js'; -import { UsedUsername } from '@/models/entities/used-username.js'; -import { Announcement } from '@/models/entities/announcement.js'; -import { AnnouncementRead } from '@/models/entities/announcement-read.js'; -import { Clip } from '@/models/entities/clip.js'; -import { ClipNote } from '@/models/entities/clip-note.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { AntennaNote } from '@/models/entities/antenna-note.js'; -import { PromoNote } from '@/models/entities/promo-note.js'; -import { PromoRead } from '@/models/entities/promo-read.js'; -import { Relay } from '@/models/entities/relay.js'; -import { MutedNote } from '@/models/entities/muted-note.js'; -import { Channel } from '@/models/entities/channel.js'; -import { ChannelFollowing } from '@/models/entities/channel-following.js'; -import { ChannelNotePining } from '@/models/entities/channel-note-pining.js'; -import { RegistryItem } from '@/models/entities/registry-item.js'; -import { Ad } from '@/models/entities/ad.js'; -import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; -import { UserPending } from '@/models/entities/user-pending.js'; -import { Webhook } from '@/models/entities/webhook.js'; -import { UserIp } from '@/models/entities/user-ip.js'; - -import { entities as charts } from '@/services/chart/entities.js'; -import { envOption } from '../env.js'; -import { dbLogger } from './logger.js'; -import { redisClient } from './redis.js'; - -const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); - -class MyCustomLogger implements Logger { - private highlight(sql: string) { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); - } - - public logQuery(query: string, parameters?: any[]) { - sqlLogger.info(this.highlight(query).substring(0, 100)); - } - - public logQueryError(error: string, query: string, parameters?: any[]) { - sqlLogger.error(this.highlight(query)); - } - - public logQuerySlow(time: number, query: string, parameters?: any[]) { - sqlLogger.warn(this.highlight(query)); - } - - public logSchemaBuild(message: string) { - sqlLogger.info(message); - } - - public log(message: string) { - sqlLogger.info(message); - } - - public logMigration(message: string) { - sqlLogger.info(message); - } -} - -export const entities = [ - Announcement, - AnnouncementRead, - Meta, - Instance, - App, - AuthSession, - AccessToken, - User, - UserProfile, - UserKeypair, - UserPublickey, - UserList, - UserListJoining, - UserGroup, - UserGroupJoining, - UserGroupInvitation, - UserNotePining, - UserSecurityKey, - UsedUsername, - AttestationChallenge, - Following, - FollowRequest, - Muting, - Blocking, - Note, - NoteFavorite, - NoteReaction, - NoteWatching, - NoteThreadMuting, - NoteUnread, - Page, - PageLike, - GalleryPost, - GalleryLike, - DriveFile, - DriveFolder, - Poll, - PollVote, - Notification, - Emoji, - Hashtag, - SwSubscription, - AbuseUserReport, - RegistrationTicket, - MessagingMessage, - Signin, - ModerationLog, - Clip, - ClipNote, - Antenna, - AntennaNote, - PromoNote, - PromoRead, - Relay, - MutedNote, - Channel, - ChannelFollowing, - ChannelNotePining, - RegistryItem, - Ad, - PasswordResetRequest, - UserPending, - Webhook, - UserIp, - ...charts, -]; - -const log = process.env.NODE_ENV !== 'production'; - -export const db = new DataSource({ - type: 'postgres', - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - extra: { - statement_timeout: 1000 * 10, - ...config.db.extra, - }, - synchronize: process.env.NODE_ENV === 'test', - dropSchema: process.env.NODE_ENV === 'test', - cache: !config.db.disableCache ? { - type: 'ioredis', - options: { - host: config.redis.host, - port: config.redis.port, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:query:`, - db: config.redis.db || 0, - }, - } : false, - logging: log, - logger: log ? new MyCustomLogger() : undefined, - maxQueryExecutionTime: 300, - entities: entities, - migrations: ['../../migration/*.js'], -}); - -export async function initDb(force = false) { - if (force) { - if (db.isInitialized) { - await db.destroy(); - } - await db.initialize(); - return; - } - - if (db.isInitialized) { - // nop - } else { - await db.initialize(); - } -} - -export async function resetDb() { - const reset = async () => { - await redisClient.flushdb(); - const tables = await db.query(`SELECT relname AS "table" - FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE nspname NOT IN ('pg_catalog', 'information_schema') - AND C.relkind = 'r' - AND nspname !~ '^pg_toast';`); - for (const table of tables) { - await db.query(`DELETE FROM "${table.table}" CASCADE`); - } - }; - - for (let i = 1; i <= 3; i++) { - try { - await reset(); - } catch (e) { - if (i === 3) { - throw e; - } else { - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - } - break; - } -} diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts deleted file mode 100644 index 49f5bb2ba8..0000000000 --- a/packages/backend/src/db/redis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Redis from 'ioredis'; -import config from '@/config/index.js'; - -export function createConnection() { - return new Redis({ - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:`, - db: config.redis.db || 0, - }); -} - -export const subsdcriber = createConnection(); -subsdcriber.subscribe(config.host); - -export const redisClient = createConnection(); diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts new file mode 100644 index 0000000000..cc775a9c8a --- /dev/null +++ b/packages/backend/src/di-symbols.ts @@ -0,0 +1,72 @@ +export const DI = { + config: Symbol('config'), + db: Symbol('db'), + redis: Symbol('redis'), + redisSubscriber: Symbol('redisSubscriber'), + + //#region Repositories + usersRepository: Symbol('usersRepository'), + notesRepository: Symbol('notesRepository'), + announcementsRepository: Symbol('announcementsRepository'), + announcementReadsRepository: Symbol('announcementReadsRepository'), + appsRepository: Symbol('appsRepository'), + noteFavoritesRepository: Symbol('noteFavoritesRepository'), + noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), + noteReactionsRepository: Symbol('noteReactionsRepository'), + noteUnreadsRepository: Symbol('noteUnreadsRepository'), + pollsRepository: Symbol('pollsRepository'), + pollVotesRepository: Symbol('pollVotesRepository'), + userProfilesRepository: Symbol('userProfilesRepository'), + userKeypairsRepository: Symbol('userKeypairsRepository'), + userPendingsRepository: Symbol('userPendingsRepository'), + attestationChallengesRepository: Symbol('attestationChallengesRepository'), + userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), + userPublickeysRepository: Symbol('userPublickeysRepository'), + userListsRepository: Symbol('userListsRepository'), + userListJoiningsRepository: Symbol('userListJoiningsRepository'), + userGroupsRepository: Symbol('userGroupsRepository'), + userGroupJoiningsRepository: Symbol('userGroupJoiningsRepository'), + userGroupInvitationsRepository: Symbol('userGroupInvitationsRepository'), + userNotePiningsRepository: Symbol('userNotePiningsRepository'), + userIpsRepository: Symbol('userIpsRepository'), + usedUsernamesRepository: Symbol('usedUsernamesRepository'), + followingsRepository: Symbol('followingsRepository'), + followRequestsRepository: Symbol('followRequestsRepository'), + instancesRepository: Symbol('instancesRepository'), + emojisRepository: Symbol('emojisRepository'), + driveFilesRepository: Symbol('driveFilesRepository'), + driveFoldersRepository: Symbol('driveFoldersRepository'), + notificationsRepository: Symbol('notificationsRepository'), + metasRepository: Symbol('metasRepository'), + mutingsRepository: Symbol('mutingsRepository'), + blockingsRepository: Symbol('blockingsRepository'), + swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), + hashtagsRepository: Symbol('hashtagsRepository'), + abuseUserReportsRepository: Symbol('abuseUserReportsRepository'), + registrationTicketsRepository: Symbol('registrationTicketsRepository'), + authSessionsRepository: Symbol('authSessionsRepository'), + accessTokensRepository: Symbol('accessTokensRepository'), + signinsRepository: Symbol('signinsRepository'), + messagingMessagesRepository: Symbol('messagingMessagesRepository'), + pagesRepository: Symbol('pagesRepository'), + pageLikesRepository: Symbol('pageLikesRepository'), + galleryPostsRepository: Symbol('galleryPostsRepository'), + galleryLikesRepository: Symbol('galleryLikesRepository'), + moderationLogsRepository: Symbol('moderationLogsRepository'), + clipsRepository: Symbol('clipsRepository'), + clipNotesRepository: Symbol('clipNotesRepository'), + antennasRepository: Symbol('antennasRepository'), + antennaNotesRepository: Symbol('antennaNotesRepository'), + promoNotesRepository: Symbol('promoNotesRepository'), + promoReadsRepository: Symbol('promoReadsRepository'), + relaysRepository: Symbol('relaysRepository'), + mutedNotesRepository: Symbol('mutedNotesRepository'), + channelsRepository: Symbol('channelsRepository'), + channelFollowingsRepository: Symbol('channelFollowingsRepository'), + channelNotePiningsRepository: Symbol('channelNotePiningsRepository'), + registryItemsRepository: Symbol('registryItemsRepository'), + webhooksRepository: Symbol('webhooksRepository'), + adsRepository: Symbol('adsRepository'), + passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), + //#endregion +}; diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts new file mode 100644 index 0000000000..6722220681 --- /dev/null +++ b/packages/backend/src/logger.ts @@ -0,0 +1,112 @@ +import cluster from 'node:cluster'; +import chalk from 'chalk'; +import { default as convertColor } from 'color-convert'; +import { format as dateFormat } from 'date-fns'; +import { envOption } from './env.js'; + +type Domain = { + name: string; + color?: string; +}; + +type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; + +export default class Logger { + private domain: Domain; + private parentLogger: Logger | null = null; + private store: boolean; + private syslogClient: any | null = null; + + constructor(domain: string, color?: string, store = true, syslogClient = null) { + this.domain = { + name: domain, + color: color, + }; + this.store = store; + this.syslogClient = syslogClient; + } + + public createSubLogger(domain: string, color?: string, store = true): Logger { + const logger = new Logger(domain, color, store); + logger.parentLogger = this; + return logger; + } + + 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; + if (level === 'debug') store = false; + + if (this.parentLogger) { + this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store); + return; + } + + const time = dateFormat(new Date(), 'HH:mm:ss'); + const worker = cluster.isPrimary ? '*' : cluster.worker.id; + const l = + level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : + level === 'warning' ? chalk.yellow('WARN') : + level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') : + level === 'debug' ? chalk.gray('VERB') : + level === 'info' ? chalk.blue('INFO') : + null; + const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.rgb(...convertColor.keyword.rgb(d.color))(d.name) : chalk.white(d.name)); + const m = + level === 'error' ? chalk.red(message) : + level === 'warning' ? chalk.yellow(message) : + level === 'success' ? chalk.green(message) : + level === 'debug' ? chalk.gray(message) : + level === 'info' ? message : + null; + + let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`; + if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; + + console.log(important ? chalk.bold(log) : log); + + if (store) { + if (this.syslogClient) { + const send = + level === 'error' ? this.syslogClient.error : + level === 'warning' ? this.syslogClient.warning : + level === 'success' ? this.syslogClient.info : + level === 'debug' ? this.syslogClient.info : + level === 'info' ? this.syslogClient.info : + null as never; + + send.bind(this.syslogClient)(message).catch(() => {}); + } + } + } + + public error(x: string | Error, data?: Record | null, important = false): void { // 実行を継続できない状況で使う + if (x instanceof Error) { + data = data ?? {}; + data.e = x; + this.log('error', x.toString(), data, important); + } else if (typeof x === 'object') { + this.log('error', `${(x as any).message ?? (x as any).name ?? x}`, data, important); + } else { + this.log('error', `${x}`, data, important); + } + } + + public warn(message: string, data?: Record | null, important = false): void { // 実行を継続できるが改善すべき状況で使う + this.log('warning', message, data, important); + } + + public succ(message: string, data?: Record | null, important = false): void { // 何かに成功した状況で使う + this.log('success', message, data, important); + } + + public debug(message: string, data?: Record | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) + if (process.env.NODE_ENV !== 'production' || envOption.verbose) { + this.log('debug', message, data, important); + } + } + + public info(message: string, data?: Record | null, important = false): void { // それ以外 + this.log('info', message, data, important); + } +} diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts deleted file mode 100644 index 7751bac563..0000000000 --- a/packages/backend/src/mfm/from-html.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { URL } from 'node:url'; -import * as parse5 from 'parse5'; -import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; - -const treeAdapter = TreeAdapter.defaultTreeAdapter; - -const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; -const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; - -export function 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'); - - const dom = parse5.parseFragment(html); - - let text = ''; - - for (const n of dom.childNodes) { - analyze(n); - } - - return text.trim(); - - function getText(node: TreeAdapter.Node): string { - if (treeAdapter.isTextNode(node)) return node.value; - if (!treeAdapter.isElementNode(node)) return ''; - if (node.nodeName === 'br') return '\n'; - - if (node.childNodes) { - return node.childNodes.map(n => getText(n)).join(''); - } - - return ''; - } - - function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { - if (childNodes) { - for (const n of childNodes) { - analyze(n); - } - } - } - - function analyze(node: TreeAdapter.Node) { - if (treeAdapter.isTextNode(node)) { - text += node.value; - return; - } - - // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) return; - - switch (node.nodeName) { - case 'br': { - text += '\n'; - break; - } - - case 'a': - { - const txt = getText(node); - const rel = node.attrs.find(x => x.name === 'rel'); - const href = node.attrs.find(x => x.name === 'href'); - - // ハッシュタグ - if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { - text += txt; - // メンション - } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { - const part = txt.split('@'); - - if (part.length === 2 && href) { - //#region ホスト名部分が省略されているので復元する - const acct = `${txt}@${(new URL(href.value)).hostname}`; - text += acct; - //#endregion - } else if (part.length === 3) { - text += txt; - } - // その他 - } else { - const generateLink = () => { - if (!href && !txt) { - return ''; - } - if (!href) { - return txt; - } - if (!txt || txt === href.value) { // #6383: Missing text node - if (href.value.match(urlRegexFull)) { - return href.value; - } else { - return `<${href.value}>`; - } - } - if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { - return `[${txt}](<${href.value}>)`; // #6846 - } else { - return `[${txt}](${href.value})`; - } - }; - - text += generateLink(); - } - break; - } - - case 'h1': - { - text += '【'; - appendChildren(node.childNodes); - text += '】\n'; - break; - } - - case 'b': - case 'strong': - { - text += '**'; - appendChildren(node.childNodes); - text += '**'; - break; - } - - case 'small': - { - text += ''; - appendChildren(node.childNodes); - text += ''; - break; - } - - case 's': - case 'del': - { - text += '~~'; - appendChildren(node.childNodes); - text += '~~'; - break; - } - - case 'i': - case 'em': - { - text += ''; - appendChildren(node.childNodes); - text += ''; - break; - } - - // block code (
)
-			case 'pre': {
-				if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
-					text += '\n```\n';
-					text += getText(node.childNodes[0]);
-					text += '\n```\n';
-				} else {
-					appendChildren(node.childNodes);
-				}
-				break;
-			}
-
-			// inline code ()
-			case 'code': {
-				text += '`';
-				appendChildren(node.childNodes);
-				text += '`';
-				break;
-			}
-
-			case 'blockquote': {
-				const t = getText(node);
-				if (t) {
-					text += '\n> ';
-					text += t.split('\n').join('\n> ');
-				}
-				break;
-			}
-
-			case 'p':
-			case 'h2':
-			case 'h3':
-			case 'h4':
-			case 'h5':
-			case 'h6':
-			{
-				text += '\n\n';
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			// other block elements
-			case 'div':
-			case 'header':
-			case 'footer':
-			case 'article':
-			case 'li':
-			case 'dt':
-			case 'dd':
-			{
-				text += '\n';
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			default:	// includes inline elements
-			{
-				appendChildren(node.childNodes);
-				break;
-			}
-		}
-	}
-}
diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts
deleted file mode 100644
index bcb5c86a3d..0000000000
--- a/packages/backend/src/mfm/to-html.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { JSDOM } from 'jsdom';
-import * as mfm from 'mfm-js';
-import config from '@/config/index.js';
-import { intersperse } from '@/prelude/array.js';
-import { IMentionedRemoteUsers } from '@/models/entities/note.js';
-
-export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
-	if (nodes == null) {
-		return null;
-	}
-
-	const { window } = new JSDOM('');
-
-	const doc = window.document;
-
-	function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
-		if (children) {
-			for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
-		}
-	}
-
-	const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
-		bold(node) {
-			const el = doc.createElement('b');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		small(node) {
-			const el = doc.createElement('small');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		strike(node) {
-			const el = doc.createElement('del');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		italic(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		fn(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		blockCode(node) {
-			const pre = doc.createElement('pre');
-			const inner = doc.createElement('code');
-			inner.textContent = node.props.code;
-			pre.appendChild(inner);
-			return pre;
-		},
-
-		center(node) {
-			const el = doc.createElement('div');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		emojiCode(node) {
-			return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
-		},
-
-		unicodeEmoji(node) {
-			return doc.createTextNode(node.props.emoji);
-		},
-
-		hashtag(node) {
-			const a = doc.createElement('a');
-			a.href = `${config.url}/tags/${node.props.hashtag}`;
-			a.textContent = `#${node.props.hashtag}`;
-			a.setAttribute('rel', 'tag');
-			return a;
-		},
-
-		inlineCode(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.code;
-			return el;
-		},
-
-		mathInline(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		mathBlock(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		link(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			appendChildren(node.children, a);
-			return a;
-		},
-
-		mention(node) {
-			const a = doc.createElement('a');
-			const { username, host, acct } = node.props;
-			const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
-			a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`;
-			a.className = 'u-url mention';
-			a.textContent = acct;
-			return a;
-		},
-
-		quote(node) {
-			const el = doc.createElement('blockquote');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		text(node) {
-			const el = doc.createElement('span');
-			const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
-
-			for (const x of intersperse('br', nodes)) {
-				el.appendChild(x === 'br' ? doc.createElement('br') : x);
-			}
-
-			return el;
-		},
-
-		url(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			a.textContent = node.props.url;
-			return a;
-		},
-
-		search(node) {
-			const a = doc.createElement('a');
-			a.href = `https://www.google.com/search?q=${node.props.query}`;
-			a.textContent = node.props.content;
-			return a;
-		},
-
-		plain(node) {
-			const el = doc.createElement('span');
-			appendChildren(node.children, el);
-			return el;
-		},
-	};
-
-	appendChildren(nodes, doc.body);
-
-	return `

${doc.body.innerHTML}

`; -} diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts index c32cee86c9..d1a6852a95 100644 --- a/packages/backend/src/misc/acct.ts +++ b/packages/backend/src/misc/acct.ts @@ -6,7 +6,7 @@ export type Acct = { export function parse(acct: string): Acct { if (acct.startsWith('@')) acct = acct.substr(1); const split = acct.split('@', 2); - return { username: split[0], host: split[1] || null }; + return { username: split[0], host: split[1] ?? null }; } export function toString(acct: Acct): string { diff --git a/packages/backend/src/misc/antenna-cache.ts b/packages/backend/src/misc/antenna-cache.ts deleted file mode 100644 index dcf96c1610..0000000000 --- a/packages/backend/src/misc/antenna-cache.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Antennas } from '@/models/index.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { subsdcriber } from '../db/redis.js'; - -let antennasFetched = false; -let antennas: Antenna[] = []; - -export async function getAntennas() { - if (!antennasFetched) { - antennas = await Antennas.find(); - antennasFetched = true; - } - - return antennas; -} - -subsdcriber.on('message', async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message; - switch (type) { - case 'antennaCreated': - antennas.push(body); - break; - case 'antennaUpdated': - antennas[antennas.findIndex(a => a.id === body.id)] = body; - break; - case 'antennaDeleted': - antennas = antennas.filter(a => a.id !== body.id); - break; - default: - break; - } - } -}); diff --git a/packages/backend/src/misc/app-lock.ts b/packages/backend/src/misc/app-lock.ts deleted file mode 100644 index b5089cc6a6..0000000000 --- a/packages/backend/src/misc/app-lock.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { redisClient } from '../db/redis.js'; -import { promisify } from 'node:util'; -import redisLock from 'redis-lock'; - -/** - * Retry delay (ms) for lock acquisition - */ -const retryDelay = 100; - -const lock: (key: string, timeout?: number) => Promise<() => void> - = redisClient - ? promisify(redisLock(redisClient, retryDelay)) - : async () => () => { }; - -/** - * Get AP Object lock - * @param uri AP object ID - * @param timeout Lock timeout (ms), The timeout releases previous lock. - * @returns Unlock function - */ -export function getApLock(uri: string, timeout = 30 * 1000) { - return lock(`ap-object:${uri}`, timeout); -} - -export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) { - return lock(`instance:${host}`, timeout); -} - -export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) { - return lock(`chart-insert:${lockKey}`, timeout); -} diff --git a/packages/backend/src/misc/before-shutdown.ts b/packages/backend/src/misc/before-shutdown.ts index 93ac7a1f39..74489293d7 100644 --- a/packages/backend/src/misc/before-shutdown.ts +++ b/packages/backend/src/misc/before-shutdown.ts @@ -65,7 +65,7 @@ async function shutdownHandler(signalOrEvent: string) { await listener(signalOrEvent); } catch (err) { if (err instanceof Error) { - console.warn(`A shutdown handler failed before completing with: ${err.message || err}`); + console.warn(`A shutdown handler failed before completing with: ${err.message ?? err}`); } } } diff --git a/packages/backend/src/misc/captcha.ts b/packages/backend/src/misc/captcha.ts deleted file mode 100644 index 9a87a4a3c8..0000000000 --- a/packages/backend/src/misc/captcha.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fetch from 'node-fetch'; -import { URLSearchParams } from 'node:url'; -import { getAgentByUrl } from './fetch.js'; -import config from '@/config/index.js'; - -export async function verifyRecaptcha(secret: string, response: string) { - const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => { - throw `recaptcha-request-failed: ${e}`; - }); - - if (result.success !== true) { - const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : ''; - throw `recaptcha-failed: ${errorCodes}`; - } -} - -export async function verifyHcaptcha(secret: string, response: string) { - const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => { - throw `hcaptcha-request-failed: ${e}`; - }); - - if (result.success !== true) { - const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : ''; - throw `hcaptcha-failed: ${errorCodes}`; - } -} - -type CaptchaResponse = { - success: boolean; - 'error-codes'?: string[]; -}; - -async function getCaptchaResponse(url: string, secret: string, response: string): Promise { - const params = new URLSearchParams({ - secret, - response, - }); - - const res = await fetch(url, { - method: 'POST', - body: params, - headers: { - 'User-Agent': config.userAgent, - }, - // TODO - //timeout: 10 * 1000, - agent: getAgentByUrl, - }).catch(e => { - throw `${e.message || e}`; - }); - - if (!res.ok) { - throw `${res.status}`; - } - - return await res.json() as CaptchaResponse; -} diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts deleted file mode 100644 index d9cedee7df..0000000000 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Antenna } from '@/models/entities/antenna.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; -import { getFullApAccount } from './convert-host.js'; -import * as Acct from '@/misc/acct.js'; -import { Packed } from './schema.js'; -import { Cache } from './cache.js'; - -const blockingCache = new Cache(1000 * 60 * 5); - -// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている - -/** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい - */ -export async function 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; - - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); - if (blockings.some(blocking => blocking === antenna.userId)) return false; - - if (note.visibility === 'followers') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; - } - - if (!antenna.withReplies && note.replyId != null) return false; - - if (antenna.src === 'home') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; - } else if (antenna.src === 'list') { - const listUsers = (await UserListJoinings.findBy({ - userListId: antenna.userListId!, - })).map(x => x.userId); - - if (!listUsers.includes(note.userId)) return false; - } else if (antenna.src === 'group') { - const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! }); - - const groupUsers = (await UserGroupJoinings.findBy({ - userGroupId: joining.userGroupId, - })).map(x => x.userId); - - if (!groupUsers.includes(note.userId)) return false; - } else if (antenna.src === 'users') { - const accts = antenna.users.map(x => { - const { username, host } = Acct.parse(x); - return getFullApAccount(username, host).toLowerCase(); - }); - if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; - } - - const keywords = antenna.keywords - // Clean up - .map(xs => xs.filter(x => x !== '')) - .filter(xs => xs.length > 0); - - if (keywords.length > 0) { - if (note.text == null) return false; - - const matched = keywords.some(and => - and.every(keyword => - antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()) - )); - - if (!matched) return false; - } - - const excludeKeywords = antenna.excludeKeywords - // Clean up - .map(xs => xs.filter(x => x !== '')) - .filter(xs => xs.length > 0); - - if (excludeKeywords.length > 0) { - if (note.text == null) return false; - - const matched = excludeKeywords.some(and => - and.every(keyword => - antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()) - )); - - if (matched) return false; - } - - if (antenna.withFile) { - if (note.fileIds && note.fileIds.length === 0) return false; - } - - // TODO: eval expression - - return true; -} diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index d7662820af..d10aca9e88 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -1,6 +1,6 @@ import RE2 from 're2'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; type NoteLike = { userId: Note['userId']; diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts deleted file mode 100644 index 7eb940a7e0..0000000000 --- a/packages/backend/src/misc/convert-host.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { URL } from 'node:url'; -import config from '@/config/index.js'; -import { toASCII } from 'punycode'; - -export function getFullApAccount(username: string, host: string | null) { - return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; -} - -export function isSelfHost(host: string) { - if (host == null) return true; - return toPuny(config.host) === toPuny(host); -} - -export function extractDbHost(uri: string) { - const url = new URL(uri); - return toPuny(url.hostname); -} - -export function toPuny(host: string) { - return toASCII(host.toLowerCase()); -} - -export function toPunyNullable(host: string | null | undefined): string | null { - if (host == null) return null; - return toASCII(host.toLowerCase()); -} diff --git a/packages/backend/src/misc/count-same-renotes.ts b/packages/backend/src/misc/count-same-renotes.ts deleted file mode 100644 index b7f8ce90c8..0000000000 --- a/packages/backend/src/misc/count-same-renotes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Notes } from '@/models/index.js'; - -export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { - // 指定したユーザーの指定したノートのリノートがいくつあるか数える - const query = Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId }) - .andWhere('note.renoteId = :renoteId', { renoteId }); - - // 指定した投稿を除く - if (excludeNoteId) { - query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); - } - - return await query.getCount(); -} diff --git a/packages/backend/src/misc/detect-url-mime.ts b/packages/backend/src/misc/detect-url-mime.ts deleted file mode 100644 index cd143cf2fb..0000000000 --- a/packages/backend/src/misc/detect-url-mime.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createTemp } from './create-temp.js'; -import { downloadUrl } from './download-url.js'; -import { detectType } from './get-file-info.js'; - -export async function detectUrlMime(url: string) { - const [path, cleanup] = await createTemp(); - - try { - await downloadUrl(url, path); - const { mime } = await detectType(path); - return mime; - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/misc/download-text-file.ts b/packages/backend/src/misc/download-text-file.ts deleted file mode 100644 index c62c70ee33..0000000000 --- a/packages/backend/src/misc/download-text-file.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as fs from 'node:fs'; -import * as util from 'node:util'; -import Logger from '@/services/logger.js'; -import { createTemp } from './create-temp.js'; -import { downloadUrl } from './download-url.js'; - -const logger = new Logger('download-text-file'); - -export async function downloadTextFile(url: string): Promise { - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - // write content at URL to temp file - await downloadUrl(url, path); - - const text = await util.promisify(fs.readFile)(path, 'utf8'); - - return text; - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/misc/download-url.ts b/packages/backend/src/misc/download-url.ts deleted file mode 100644 index 7c57b140ef..0000000000 --- a/packages/backend/src/misc/download-url.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as fs from 'node:fs'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; -import got, * as Got from 'got'; -import { httpAgent, httpsAgent, StatusError } from './fetch.js'; -import config from '@/config/index.js'; -import chalk from 'chalk'; -import Logger from '@/services/logger.js'; -import IPCIDR from 'ip-cidr'; -import PrivateIp from 'private-ip'; - -const pipeline = util.promisify(stream.pipeline); - -export async function downloadUrl(url: string, path: string): Promise { - const logger = new Logger('download'); - - logger.info(`Downloading ${chalk.cyan(url)} ...`); - - const timeout = 30 * 1000; - const operationTimeout = 60 * 1000; - const maxSize = config.maxFileSize || 262144000; - - const req = got.stream(url, { - headers: { - 'User-Agent': config.userAgent, - }, - timeout: { - lookup: timeout, - connect: timeout, - secureConnect: timeout, - socket: timeout, // read timeout - response: timeout, - send: timeout, - request: operationTimeout, // whole operation timeout - }, - agent: { - http: httpAgent, - https: httpsAgent, - }, - http2: false, // default - retry: { - limit: 0, - }, - }).on('response', (res: Got.Response) => { - if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) { - if (isPrivateIp(res.ip)) { - logger.warn(`Blocked address: ${res.ip}`); - req.destroy(); - } - } - - const contentLength = res.headers['content-length']; - if (contentLength != null) { - const size = Number(contentLength); - if (size > maxSize) { - logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); - req.destroy(); - } - } - }).on('downloadProgress', (progress: Got.Progress) => { - if (progress.transferred > maxSize) { - logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); - req.destroy(); - } - }); - - try { - await pipeline(req, fs.createWriteStream(path)); - } catch (e) { - if (e instanceof Got.HTTPError) { - throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); - } else { - throw e; - } - } - - logger.succ(`Download finished: ${chalk.cyan(url)}`); -} - -function isPrivateIp(ip: string): boolean { - for (const net of config.allowedPrivateNetworks || []) { - const cidr = new IPCIDR(net); - if (cidr.contains(ip)) { - return false; - } - } - - return PrivateIp(ip); -} diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index a0319d8dd5..8fb3f4b19e 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -1,5 +1,5 @@ import * as mfm from 'mfm-js'; -import { unique } from '@/prelude/array.js'; +import { unique } from '@/misc/prelude/array.js'; export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { const emojiNodes = mfm.extract(nodes, (node) => { diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index 0b0418eefd..f8cabda3d6 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -1,5 +1,5 @@ import * as mfm from 'mfm-js'; -import { unique } from '@/prelude/array.js'; +import { unique } from '@/misc/prelude/array.js'; export function extractHashtags(nodes: mfm.MfmNode[]): string[] { const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag'); diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index cc19b161a8..c8762e797b 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -4,7 +4,7 @@ import * as mfm from 'mfm-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); + const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention') as mfm.MfmMention[]; const mentions = mentionNodes.map(x => x.props); return mentions; diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts deleted file mode 100644 index e855ac28ee..0000000000 --- a/packages/backend/src/misc/fetch-meta.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Meta } from '@/models/entities/meta.js'; - -let cache: Meta; - -export async function fetchMeta(noCache = false): Promise { - if (!noCache && cache) return cache; - - return await db.transaction(async transactionalEntityManager => { - // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: 'DESC', - }, - }); - - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: 'x', - }, - ['id'], - ) - .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); - - cache = saved; - return saved; - } - }); -} - -setInterval(() => { - fetchMeta(true).then(meta => { - cache = meta; - }); -}, 1000 * 10); diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts deleted file mode 100644 index b61bba264b..0000000000 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { fetchMeta } from './fetch-meta.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; - -export async function fetchProxyAccount(): Promise { - const meta = await fetchMeta(); - if (meta.proxyAccountId == null) return null; - return await Users.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser; -} diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts deleted file mode 100644 index af6bf2fca7..0000000000 --- a/packages/backend/src/misc/fetch.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as http from 'node:http'; -import * as https from 'node:https'; -import { URL } from 'node:url'; -import CacheableLookup from 'cacheable-lookup'; -import fetch from 'node-fetch'; -import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; -import config from '@/config/index.js'; - -export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record) { - const res = await getResponse({ - url, - method: 'GET', - headers: Object.assign({ - 'User-Agent': config.userAgent, - Accept: accept, - }, headers || {}), - timeout, - }); - - return await res.json(); -} - -export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record) { - const res = await getResponse({ - url, - method: 'GET', - headers: Object.assign({ - 'User-Agent': config.userAgent, - Accept: accept, - }, headers || {}), - timeout, - }); - - return await res.text(); -} - -export async function getResponse(args: { url: string, method: string, body?: string, headers: Record, timeout?: number, size?: number }) { - const timeout = args.timeout || 10 * 1000; - - const controller = new AbortController(); - setTimeout(() => { - controller.abort(); - }, timeout * 6); - - const res = await fetch(args.url, { - method: args.method, - headers: args.headers, - body: args.body, - timeout, - size: args.size || 10 * 1024 * 1024, - agent: getAgentByUrl, - signal: controller.signal, - }); - - if (!res.ok) { - throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); - } - - return res; -} - -const cache = new CacheableLookup({ - maxTtl: 3600, // 1hours - errorTtl: 30, // 30secs - lookup: false, // nativeのdns.lookupにfallbackしない -}); - -/** - * Get http non-proxy agent - */ -const _http = new http.Agent({ - keepAlive: true, - keepAliveMsecs: 30 * 1000, - lookup: cache.lookup, -} as http.AgentOptions); - -/** - * Get https non-proxy agent - */ -const _https = new https.Agent({ - keepAlive: true, - keepAliveMsecs: 30 * 1000, - lookup: cache.lookup, -} as https.AgentOptions); - -const maxSockets = Math.max(256, config.deliverJobConcurrency || 128); - -/** - * Get http proxy or non-proxy agent - */ -export const httpAgent = config.proxy - ? new HttpProxyAgent({ - keepAlive: true, - keepAliveMsecs: 30 * 1000, - maxSockets, - maxFreeSockets: 256, - scheduling: 'lifo', - proxy: config.proxy, - }) - : _http; - -/** - * Get https proxy or non-proxy agent - */ -export const httpsAgent = config.proxy - ? new HttpsProxyAgent({ - keepAlive: true, - keepAliveMsecs: 30 * 1000, - maxSockets, - maxFreeSockets: 256, - scheduling: 'lifo', - proxy: config.proxy, - }) - : _https; - -/** - * Get agent by URL - * @param url URL - * @param bypassProxy Allways bypass proxy - */ -export function getAgentByUrl(url: URL, bypassProxy = false) { - if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) { - return url.protocol === 'http:' ? _http : _https; - } else { - return url.protocol === 'http:' ? httpAgent : httpsAgent; - } -} - -export class StatusError extends Error { - public statusCode: number; - public statusMessage?: string; - public isClientError: boolean; - - constructor(message: string, statusCode: number, statusMessage?: string) { - super(message); - this.name = 'StatusError'; - this.statusCode = statusCode; - this.statusMessage = statusMessage; - this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; - } -} diff --git a/packages/backend/src/misc/gen-id.ts b/packages/backend/src/misc/gen-id.ts deleted file mode 100644 index fcf476857f..0000000000 --- a/packages/backend/src/misc/gen-id.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ulid } from 'ulid'; -import { genAid } from './id/aid.js'; -import { genMeid } from './id/meid.js'; -import { genMeidg } from './id/meidg.js'; -import { genObjectId } from './id/object-id.js'; -import config from '@/config/index.js'; - -const metohd = config.id.toLowerCase(); - -export function genId(date?: Date): string { - if (!date || (date > new Date())) date = new Date(); - - switch (metohd) { - case 'aid': return genAid(date); - case 'meid': return genMeid(date); - case 'meidg': return genMeidg(date); - case 'ulid': return ulid(date.getTime()); - case 'objectid': return genObjectId(date); - default: throw new Error('unrecognized id generation method'); - } -} diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts new file mode 100644 index 0000000000..5d8a4c5378 --- /dev/null +++ b/packages/backend/src/misc/generate-native-user-token.ts @@ -0,0 +1,3 @@ +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +export default () => secureRndstr(16, true); diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts deleted file mode 100644 index 1c988b2487..0000000000 --- a/packages/backend/src/misc/get-file-info.ts +++ /dev/null @@ -1,374 +0,0 @@ -import * as fs from 'node:fs'; -import * as crypto from 'node:crypto'; -import { join } from 'node:path'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; -import { FSWatcher } from 'chokidar'; -import { fileTypeFromFile } from 'file-type'; -import FFmpeg from 'fluent-ffmpeg'; -import isSvg from 'is-svg'; -import probeImageSize from 'probe-image-size'; -import { type predictionType } from 'nsfwjs'; -import sharp from 'sharp'; -import { encode } from 'blurhash'; -import { detectSensitive } from '@/services/detect-sensitive.js'; -import { createTempDir } from './create-temp.js'; - -const pipeline = util.promisify(stream.pipeline); - -export type FileInfo = { - size: number; - md5: string; - type: { - mime: string; - ext: string | null; - }; - width?: number; - height?: number; - orientation?: number; - blurhash?: string; - sensitive: boolean; - porn: boolean; - warnings: string[]; -}; - -const TYPE_OCTET_STREAM = { - mime: 'application/octet-stream', - ext: null, -}; - -const TYPE_SVG = { - mime: 'image/svg+xml', - ext: 'svg', -}; - -/** - * Get file information - */ -export async function getFileInfo(path: string, opts: { - skipSensitiveDetection: boolean; - sensitiveThreshold?: number; - sensitiveThresholdForPorn?: number; - enableSensitiveMediaDetectionForVideos?: boolean; -}): Promise { - const warnings = [] as string[]; - - const size = await getFileSize(path); - const md5 = await calcHash(path); - - let type = await detectType(path); - - // image dimensions - let width: number | undefined; - let height: number | undefined; - let orientation: number | undefined; - - if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { - const imageSize = await detectImageSize(path).catch(e => { - warnings.push(`detectImageSize failed: ${e}`); - return undefined; - }); - - // うまく判定できない画像は octet-stream にする - if (!imageSize) { - warnings.push('cannot detect image dimensions'); - type = TYPE_OCTET_STREAM; - } else if (imageSize.wUnits === 'px') { - width = imageSize.width; - height = imageSize.height; - orientation = imageSize.orientation; - - // 制限を超えている画像は octet-stream にする - if (imageSize.width > 16383 || imageSize.height > 16383) { - warnings.push('image dimensions exceeds limits'); - type = TYPE_OCTET_STREAM; - } - } else { - warnings.push(`unsupported unit type: ${imageSize.wUnits}`); - } - } - - let blurhash: string | undefined; - - if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) { - blurhash = await getBlurhash(path).catch(e => { - warnings.push(`getBlurhash failed: ${e}`); - return undefined; - }); - } - - let sensitive = false; - let porn = false; - - if (!opts.skipSensitiveDetection) { - await detectSensitivity( - path, - type.mime, - opts.sensitiveThreshold ?? 0.5, - opts.sensitiveThresholdForPorn ?? 0.75, - opts.enableSensitiveMediaDetectionForVideos ?? false, - ).then(value => { - [sensitive, porn] = value; - }, error => { - warnings.push(`detectSensitivity failed: ${error}`); - }); - } - - return { - size, - md5, - type, - width, - height, - orientation, - blurhash, - sensitive, - porn, - warnings, - }; -} - -async function detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { - let sensitive = false; - let porn = false; - - function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { - let sensitive = false; - let porn = false; - - if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; - if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; - if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; - - if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; - - return [sensitive, porn]; - } - - if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) { - const result = await detectSensitive(source); - if (result) { - [sensitive, porn] = judgePrediction(result); - } - } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { - const [outDir, disposeOutDir] = await createTempDir(); - try { - const command = FFmpeg() - .input(source) - .inputOptions([ - '-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) - '-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) - ]) - .noAudio() - .videoFilters([ - { - filter: 'select', // フレームのフィルタリング - options: { - e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい) - }, - }, - { - filter: 'blackframe', // 暗いフレームの検出 - options: { - amount: '0', // 暗さに関わらず全てのフレームで測定値を取る - }, - }, - { - filter: 'metadata', - options: { - mode: 'select', // フレーム選択モード - key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) - value: '50', - function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので) - }, - }, - { - filter: 'scale', - options: { - w: 299, - h: 299, - }, - }, - ]) - .format('image2') - .output(join(outDir, '%d.png')) - .outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない - const results: ReturnType[] = []; - let frameIndex = 0; - let targetIndex = 0; - let nextIndex = 1; - for await (const path of asyncIterateFrames(outDir, command)) { - try { - const index = frameIndex++; - if (index !== targetIndex) { - continue; - } - targetIndex = nextIndex; - nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける - const result = await detectSensitive(path); - if (result) { - results.push(judgePrediction(result)); - } - } finally { - fs.promises.unlink(path); - } - } - sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); - porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); - } finally { - disposeOutDir(); - } - } - - return [sensitive, porn]; -} - -async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { - const watcher = new FSWatcher({ - cwd, - disableGlobbing: true, - }); - let finished = false; - command.once('end', () => { - finished = true; - watcher.close(); - }); - command.run(); - for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition - const current = `${i}.png`; - const next = `${i + 1}.png`; - const framePath = join(cwd, current); - if (await exists(join(cwd, next))) { - yield framePath; - } else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition - watcher.add(next); - await new Promise((resolve, reject) => { - watcher.on('add', function onAdd(path) { - if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている - watcher.unwatch(current); - watcher.off('add', onAdd); - resolve(); - } - }); - command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている - command.once('error', reject); - }); - yield framePath; - } else if (await exists(framePath)) { - yield framePath; - } else { - return; - } - } -} - -function exists(path: string): Promise { - return fs.promises.access(path).then(() => true, () => false); -} - -/** - * Detect MIME Type and extension - */ -export async function detectType(path: string): Promise<{ - mime: string; - ext: string | null; -}> { - // Check 0 byte - const fileSize = await getFileSize(path); - if (fileSize === 0) { - return TYPE_OCTET_STREAM; - } - - const type = await fileTypeFromFile(path); - - if (type) { - // XMLはSVGかもしれない - if (type.mime === 'application/xml' && await checkSvg(path)) { - return TYPE_SVG; - } - - return { - mime: type.mime, - ext: type.ext, - }; - } - - // 種類が不明でもSVGかもしれない - if (await checkSvg(path)) { - return TYPE_SVG; - } - - // それでも種類が不明なら application/octet-stream にする - return TYPE_OCTET_STREAM; -} - -/** - * Check the file is SVG or not - */ -export async function checkSvg(path: string) { - try { - const size = await getFileSize(path); - if (size > 1 * 1024 * 1024) return false; - return isSvg(fs.readFileSync(path)); - } catch { - return false; - } -} - -/** - * Get file size - */ -export async function getFileSize(path: string): Promise { - const getStat = util.promisify(fs.stat); - return (await getStat(path)).size; -} - -/** - * Calculate MD5 hash - */ -async function calcHash(path: string): Promise { - const hash = crypto.createHash('md5').setEncoding('hex'); - await pipeline(fs.createReadStream(path), hash); - return hash.read(); -} - -/** - * Detect dimensions of image - */ -async function detectImageSize(path: string): Promise<{ - width: number; - height: number; - wUnits: string; - hUnits: string; - orientation?: number; -}> { - const readable = fs.createReadStream(path); - const imageSize = await probeImageSize(readable); - readable.destroy(); - return imageSize; -} - -/** - * Calculate average color of image - */ -function getBlurhash(path: string): Promise { - return new Promise((resolve, reject) => { - sharp(path) - .raw() - .ensureAlpha() - .resize(64, 64, { fit: 'inside' }) - .toBuffer((err, buffer, { width, height }) => { - if (err) return reject(err); - - let hash; - - try { - hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7); - } catch (e) { - return reject(e); - } - - resolve(hash); - }); - }); -} diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 3f35ccee82..85bc2ec94d 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -1,4 +1,4 @@ -import { Packed } from './schema.js'; +import type { Packed } from './schema.js'; /** * 投稿を表す文字列を取得します。 @@ -6,11 +6,11 @@ import { Packed } from './schema.js'; */ export const getNoteSummary = (note: Packed<'Note'>): string => { if (note.deletedAt) { - return `(❌⛔)`; + return '(❌⛔)'; } if (note.isHidden) { - return `(⛔)`; + return '(⛔)'; } let summary = ''; @@ -23,13 +23,13 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { } // ファイルが添付されているとき - if ((note.files || []).length !== 0) { + if ((note.files ?? []).length !== 0) { summary += ` (📎${note.files!.length})`; } // 投票が添付されているとき if (note.poll) { - summary += ` (📊)`; + summary += ' (📊)'; } // 返信のとき diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index 2d7c6bd0c6..e394123f1b 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -7,7 +7,7 @@ export class IdentifiableError extends Error { constructor(id: string, message?: string) { super(message); - this.message = message || ''; + this.message = message ?? ''; this.id = id; } } diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts new file mode 100644 index 0000000000..2833c570c8 --- /dev/null +++ b/packages/backend/src/misc/is-native-token.ts @@ -0,0 +1 @@ +export default (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts index 779f548b03..6ea71cd878 100644 --- a/packages/backend/src/misc/is-quote.ts +++ b/packages/backend/src/misc/is-quote.ts @@ -1,4 +1,4 @@ -import { Note } from '@/models/entities/note.js'; +import { Note } from '@/models/entities/Note.js'; export default function(note: Note): boolean { return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts deleted file mode 100644 index 1183b9a781..0000000000 --- a/packages/backend/src/misc/keypair-store.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UserKeypairs } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { Cache } from './cache.js'; - -const cache = new Cache(Infinity); - -export async function getUserKeypair(userId: User['id']): Promise { - return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId: userId })); -} diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts deleted file mode 100644 index 6a185d09f6..0000000000 --- a/packages/backend/src/misc/populate-emojis.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { In, IsNull } from 'typeorm'; -import { Emojis } from '@/models/index.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { Note } from '@/models/entities/note.js'; -import { Cache } from './cache.js'; -import { isSelfHost, toPunyNullable } from './convert-host.js'; -import { decodeReaction } from './reaction-lib.js'; -import config from '@/config/index.js'; -import { query } from '@/prelude/url.js'; - -const cache = new Cache(1000 * 60 * 60 * 12); - -/** - * 添付用絵文字情報 - */ -type PopulatedEmoji = { - name: string; - url: string; -}; - -function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { - // クエリに使うホスト - let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) - : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) - : isSelfHost(src) ? null // 自ホスト指定 - : (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) - - host = toPunyNullable(host); - - return host; -} - -function parseEmojiStr(emojiName: string, noteUserHost: string | null) { - const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); - if (!match) return { name: null, host: null }; - - const name = match[1]; - - // ホスト正規化 - const host = toPunyNullable(normalizeHost(match[2], noteUserHost)); - - return { name, host }; -} - -/** - * 添付用絵文字情報を解決する - * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) - * @param noteUserHost ノートやユーザープロフィールの所有者のホスト - * @returns 絵文字情報, nullは未マッチを意味する - */ -export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { - const { name, host } = parseEmojiStr(emojiName, noteUserHost); - if (name == null) return null; - - const queryOrNull = async () => (await Emojis.findOneBy({ - name, - host: host ?? IsNull(), - })) || null; - - const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); - - if (emoji == null) return null; - - const isLocal = emoji.host == null; - const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため - const url = isLocal ? emojiUrl : `${config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; - - return { - name: emojiName, - url, - }; -} - -/** - * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される) - */ -export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise { - const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost))); - return emojis.filter((x): x is PopulatedEmoji => x != null); -} - -export function aggregateNoteEmojis(notes: Note[]) { - let emojis: { name: string | null; host: string | null; }[] = []; - for (const note of notes) { - emojis = emojis.concat(note.emojis - .map(e => parseEmojiStr(e, note.userHost))); - if (note.renote) { - emojis = emojis.concat(note.renote.emojis - .map(e => parseEmojiStr(e, note.renote!.userHost))); - if (note.renote.user) { - emojis = emojis.concat(note.renote.user.emojis - .map(e => parseEmojiStr(e, note.renote!.userHost))); - } - } - const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis; - emojis = emojis.concat(customReactions); - if (note.user) { - emojis = emojis.concat(note.user.emojis - .map(e => parseEmojiStr(e, note.userHost))); - } - } - return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[]; -} - -/** - * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します - */ -export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { - const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null); - const emojisQuery: any[] = []; - const hosts = new Set(notCachedEmojis.map(e => e.host)); - for (const host of hosts) { - emojisQuery.push({ - name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)), - host: host ?? IsNull(), - }); - } - const _emojis = emojisQuery.length > 0 ? await Emojis.find({ - where: emojisQuery, - select: ['name', 'host', 'originalUrl', 'publicUrl'], - }) : []; - for (const emoji of _emojis) { - cache.set(`${emoji.name} ${emoji.host}`, emoji); - } -} diff --git a/packages/backend/src/misc/prelude/README.md b/packages/backend/src/misc/prelude/README.md new file mode 100644 index 0000000000..bb728cfb1b --- /dev/null +++ b/packages/backend/src/misc/prelude/README.md @@ -0,0 +1,3 @@ +# Prelude +このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。 +Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。 diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts new file mode 100644 index 0000000000..0b2830cb7b --- /dev/null +++ b/packages/backend/src/misc/prelude/array.ts @@ -0,0 +1,138 @@ +import { EndoRelation, Predicate } from './relation.js'; + +/** + * Count the number of elements that satisfy the predicate + */ + +export function countIf(f: Predicate, xs: T[]): number { + return xs.filter(f).length; +} + +/** + * Count the number of elements that is equal to the element + */ +export function count(a: T, xs: T[]): number { + return countIf(x => x === a, xs); +} + +/** + * Concatenate an array of arrays + */ +export function concat(xss: T[][]): T[] { + return ([] as T[]).concat(...xss); +} + +/** + * Intersperse the element between the elements of the array + * @param sep The element to be interspersed + */ +export function intersperse(sep: T, xs: T[]): T[] { + return concat(xs.map(x => [sep, x])).slice(1); +} + +/** + * Returns the array of elements that is not equal to the element + */ +export function erase(a: T, xs: T[]): T[] { + return xs.filter(x => x !== a); +} + +/** + * Finds the array of all elements in the first array not contained in the second array. + * The order of result values are determined by the first array. + */ +export function difference(xs: T[], ys: T[]): T[] { + return xs.filter(x => !ys.includes(x)); +} + +/** + * Remove all but the first element from every group of equivalent elements + */ +export function unique(xs: T[]): T[] { + return [...new Set(xs)]; +} + +export function sum(xs: number[]): number { + return xs.reduce((a, b) => a + b, 0); +} + +export function maximum(xs: number[]): number { + return Math.max(...xs); +} + +/** + * Splits an array based on the equivalence relation. + * The concatenation of the result is equal to the argument. + */ +export function groupBy(f: EndoRelation, xs: T[]): T[][] { + const groups = [] as T[][]; + for (const x of xs) { + if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { + groups[groups.length - 1].push(x); + } else { + groups.push([x]); + } + } + return groups; +} + +/** + * Splits an array based on the equivalence relation induced by the function. + * The concatenation of the result is equal to the argument. + */ +export function groupOn(f: (x: T) => S, xs: T[]): T[][] { + return groupBy((a, b) => f(a) === f(b), xs); +} + +export function groupByX(collections: T[], keySelector: (x: T) => string) { + return collections.reduce((obj: Record, item: T) => { + const key = keySelector(item); + if (!Object.prototype.hasOwnProperty.call(obj, key)) { + obj[key] = []; + } + + obj[key].push(item); + + return obj; + }, {}); +} + +/** + * Compare two arrays by lexicographical order + */ +export function lessThan(xs: number[], ys: number[]): boolean { + for (let i = 0; i < Math.min(xs.length, ys.length); i++) { + if (xs[i] < ys[i]) return true; + if (xs[i] > ys[i]) return false; + } + return xs.length < ys.length; +} + +/** + * Returns the longest prefix of elements that satisfy the predicate + */ +export function takeWhile(f: Predicate, xs: T[]): T[] { + const ys = []; + for (const x of xs) { + if (f(x)) { + ys.push(x); + } else { + break; + } + } + return ys; +} + +export function cumulativeSum(xs: number[]): number[] { + const ys = Array.from(xs); // deep copy + for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; + return ys; +} + +export function toArray(x: T | T[] | undefined): T[] { + return Array.isArray(x) ? x : x != null ? [x] : []; +} + +export function toSingle(x: T | T[] | undefined): T | undefined { + return Array.isArray(x) ? x[0] : x; +} diff --git a/packages/backend/src/misc/prelude/await-all.ts b/packages/backend/src/misc/prelude/await-all.ts new file mode 100644 index 0000000000..b955c3a5d8 --- /dev/null +++ b/packages/backend/src/misc/prelude/await-all.ts @@ -0,0 +1,21 @@ +export type Promiseable = { + [K in keyof T]: Promise | T[K]; +}; + +export async function awaitAll(obj: Promiseable): Promise { + const target = {} as T; + const keys = Object.keys(obj) as unknown as (keyof T)[]; + const values = Object.values(obj) as any[]; + + const resolvedValues = await Promise.all(values.map(value => + (!value || !value.constructor || value.constructor.name !== 'Object') + ? value + : awaitAll(value) + )); + + for (let i = 0; i < keys.length; i++) { + target[keys[i]] = resolvedValues[i]; + } + + return target; +} diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/backend/src/misc/prelude/math.ts new file mode 100644 index 0000000000..07b94bec30 --- /dev/null +++ b/packages/backend/src/misc/prelude/math.ts @@ -0,0 +1,3 @@ +export function gcd(a: number, b: number): number { + return b === 0 ? a : gcd(b, a % b); +} diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts new file mode 100644 index 0000000000..df7c4ed52a --- /dev/null +++ b/packages/backend/src/misc/prelude/maybe.ts @@ -0,0 +1,20 @@ +export interface IMaybe { + isJust(): this is IJust; +} + +export interface IJust extends IMaybe { + get(): T; +} + +export function just(value: T): IJust { + return { + isJust: () => true, + get: () => value, + }; +} + +export function nothing(): IMaybe { + return { + isJust: () => false, + }; +} diff --git a/packages/backend/src/misc/prelude/relation.ts b/packages/backend/src/misc/prelude/relation.ts new file mode 100644 index 0000000000..1f4703f52f --- /dev/null +++ b/packages/backend/src/misc/prelude/relation.ts @@ -0,0 +1,5 @@ +export type Predicate = (a: T) => boolean; + +export type Relation = (a: T, b: U) => boolean; + +export type EndoRelation = Relation; diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts new file mode 100644 index 0000000000..b907e0a2e1 --- /dev/null +++ b/packages/backend/src/misc/prelude/string.ts @@ -0,0 +1,15 @@ +export function concat(xs: string[]): string { + return xs.join(''); +} + +export function capitalize(s: string): string { + return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1)); +} + +export function toUpperCase(s: string): string { + return s.toUpperCase(); +} + +export function toLowerCase(s: string): string { + return s.toLowerCase(); +} diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/backend/src/misc/prelude/symbol.ts new file mode 100644 index 0000000000..51e12f7450 --- /dev/null +++ b/packages/backend/src/misc/prelude/symbol.ts @@ -0,0 +1 @@ +export const fallback = Symbol('fallback'); diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts new file mode 100644 index 0000000000..34e8b6b17c --- /dev/null +++ b/packages/backend/src/misc/prelude/time.ts @@ -0,0 +1,39 @@ +const dateTimeIntervals = { + 'day': 86400000, + 'hour': 3600000, + 'ms': 1, +}; + +export function dateUTC(time: number[]): Date { + const d = time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; + + if (!d) throw 'wrong number of arguments'; + + return new Date(d); +} + +export function isTimeSame(a: Date, b: Date): boolean { + return a.getTime() === b.getTime(); +} + +export function isTimeBefore(a: Date, b: Date): boolean { + return (a.getTime() - b.getTime()) < 0; +} + +export function isTimeAfter(a: Date, b: Date): boolean { + return (a.getTime() - b.getTime()) > 0; +} + +export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { + return new Date(x.getTime() + (value * dateTimeIntervals[span])); +} + +export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { + return new Date(x.getTime() - (value * dateTimeIntervals[span])); +} diff --git a/packages/backend/src/misc/prelude/url.ts b/packages/backend/src/misc/prelude/url.ts new file mode 100644 index 0000000000..a4f2f7f5a8 --- /dev/null +++ b/packages/backend/src/misc/prelude/url.ts @@ -0,0 +1,13 @@ +export function query(obj: Record): string { + const params = Object.entries(obj) + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) + .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); + + return Object.entries(params) + .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) + .join('&'); +} + +export function appendQuery(url: string, query: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; +} diff --git a/packages/backend/src/misc/prelude/xml.ts b/packages/backend/src/misc/prelude/xml.ts new file mode 100644 index 0000000000..b4469a1d8d --- /dev/null +++ b/packages/backend/src/misc/prelude/xml.ts @@ -0,0 +1,41 @@ +const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', +}; + +const beginingOfCDATA = ''; + +export function escapeValue(x: string): string { + let insideOfCDATA = false; + let builder = ''; + for ( + let i = 0; + i < x.length; + ) { + if (insideOfCDATA) { + if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) { + insideOfCDATA = true; + i += beginingOfCDATA.length; + } else { + builder += x[i++]; + } + } else { + if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) { + insideOfCDATA = false; + i += endOfCDATA.length; + } else { + const b = x[i++]; + builder += map[b] || b; + } + } + } + return builder; +} + +export function escapeAttribute(x: string): string { + return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x); +} diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts deleted file mode 100644 index fefc2781f3..0000000000 --- a/packages/backend/src/misc/reaction-lib.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint-disable key-spacing */ -import { emojiRegex } from './emoji-regex.js'; -import { fetchMeta } from './fetch-meta.js'; -import { Emojis } from '@/models/index.js'; -import { toPunyNullable } from './convert-host.js'; -import { IsNull } from 'typeorm'; - -const legacies: Record = { - 'like': '👍', - 'love': '❤', // ここに記述する場合は異体字セレクタを入れない - 'laugh': '😆', - 'hmm': '🤔', - 'surprise': '😮', - 'congrats': '🎉', - 'angry': '💢', - 'confused': '😥', - 'rip': '😇', - 'pudding': '🍮', - 'star': '⭐', -}; - -export async function getFallbackReaction(): Promise { - const meta = await fetchMeta(); - return meta.useStarForReactionFallback ? '⭐' : '👍'; -} - -export function convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; - - for (const reaction of Object.keys(reactions)) { - if (reactions[reaction] <= 0) continue; - - if (Object.keys(legacies).includes(reaction)) { - if (_reactions[legacies[reaction]]) { - _reactions[legacies[reaction]] += reactions[reaction]; - } else { - _reactions[legacies[reaction]] = reactions[reaction]; - } - } else { - if (_reactions[reaction]) { - _reactions[reaction] += reactions[reaction]; - } else { - _reactions[reaction] = reactions[reaction]; - } - } - } - - const _reactions2 = {} as Record; - - for (const reaction of Object.keys(_reactions)) { - _reactions2[decodeReaction(reaction).reaction] = _reactions[reaction]; - } - - return _reactions2; -} - -export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise { - if (reaction == null) return await getFallbackReaction(); - - reacterHost = toPunyNullable(reacterHost); - - // 文字列タイプのリアクションを絵文字に変換 - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - - // Unicode絵文字 - const match = emojiRegex.exec(reaction); - if (match) { - // 合字を含む1つの絵文字 - const unicode = match[0]; - - // 異体字セレクタ除去 - return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); - } - - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = await Emojis.findOneBy({ - host: reacterHost ?? IsNull(), - name, - }); - - if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; - } - - return await getFallbackReaction(); -} - -type DecodedReaction = { - /** - * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') - */ - reaction: string; - - /** - * name (カスタム絵文字の場合name, Emojiクエリに使う) - */ - name?: string; - - /** - * host (カスタム絵文字の場合host, Emojiクエリに使う) - */ - host?: string | null; -}; - -export function decodeReaction(str: string): DecodedReaction { - const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); - - if (custom) { - const name = custom[1]; - const host = custom[2] || null; - - return { - reaction: `:${name}@${host || '.'}:`, // ローカル分は@以降を省略するのではなく.にする - name, - host, - }; - } - - return { - reaction: str, - name: undefined, - host: undefined, - }; -} - -export function convertLegacyReaction(reaction: string): string { - reaction = decodeReaction(reaction).reaction; - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - return reaction; -} diff --git a/packages/backend/src/misc/reset-db.ts b/packages/backend/src/misc/reset-db.ts new file mode 100644 index 0000000000..835cd2ba28 --- /dev/null +++ b/packages/backend/src/misc/reset-db.ts @@ -0,0 +1,28 @@ +import type { DataSource } from 'typeorm'; + +export async function resetDb(db: DataSource) { + const reset = async () => { + const tables = await db.query(`SELECT relname AS "table" + FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) + WHERE nspname NOT IN ('pg_catalog', 'information_schema') + AND C.relkind = 'r' + AND nspname !~ '^pg_toast';`); + for (const table of tables) { + await db.query(`DELETE FROM "${table.table}" CASCADE`); + } + }; + + for (let i = 1; i <= 3; i++) { + try { + await reset(); + } catch (e) { + if (i === 3) { + throw e; + } else { + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + } + break; + } +} diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts index bc71cfbe96..bfb1b85f33 100644 --- a/packages/backend/src/misc/show-machine-info.ts +++ b/packages/backend/src/misc/show-machine-info.ts @@ -1,6 +1,6 @@ import * as os from 'node:os'; import sysUtils from 'systeminformation'; -import Logger from '@/services/logger.js'; +import Logger from '@/core/logger.js'; export async function showMachineInfo(parentLogger: Logger) { const logger = parentLogger.createSubLogger('machine'); diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts new file mode 100644 index 0000000000..0a33f8acaf --- /dev/null +++ b/packages/backend/src/misc/status-error.ts @@ -0,0 +1,13 @@ +export class StatusError extends Error { + public statusCode: number; + public statusMessage?: string; + public isClientError: boolean; + + constructor(message: string, statusCode: number, statusMessage?: string) { + super(message); + this.name = 'StatusError'; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; + } +} diff --git a/packages/backend/src/misc/webhook-cache.ts b/packages/backend/src/misc/webhook-cache.ts deleted file mode 100644 index 4bd2333661..0000000000 --- a/packages/backend/src/misc/webhook-cache.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import { Webhook } from '@/models/entities/webhook.js'; -import { subsdcriber } from '../db/redis.js'; - -let webhooksFetched = false; -let webhooks: Webhook[] = []; - -export async function getActiveWebhooks() { - if (!webhooksFetched) { - webhooks = await Webhooks.findBy({ - active: true, - }); - webhooksFetched = true; - } - - return webhooks; -} - -subsdcriber.on('message', async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message; - switch (type) { - case 'webhookCreated': - if (body.active) { - webhooks.push(body); - } - break; - case 'webhookUpdated': - if (body.active) { - const i = webhooks.findIndex(a => a.id === body.id); - if (i > -1) { - webhooks[i] = body; - } else { - webhooks.push(body); - } - } else { - webhooks = webhooks.filter(a => a.id !== body.id); - } - break; - case 'webhookDeleted': - webhooks = webhooks.filter(a => a.id !== body.id); - break; - default: - break; - } - } -}); diff --git a/packages/backend/src/models/entities/AbuseUserReport.ts b/packages/backend/src/models/entities/AbuseUserReport.ts new file mode 100644 index 0000000000..07305cf23a --- /dev/null +++ b/packages/backend/src/models/entities/AbuseUserReport.ts @@ -0,0 +1,79 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class AbuseUserReport { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the AbuseUserReport.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public targetUserId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public targetUser: User | null; + + @Index() + @Column(id()) + public reporterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public reporter: User | null; + + @Column({ + ...id(), + nullable: true, + }) + public assigneeId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public assignee: User | null; + + @Index() + @Column('boolean', { + default: false, + }) + public resolved: boolean; + + @Column('boolean', { + default: false, + }) + public forwarded: boolean; + + @Column('varchar', { + length: 2048, + }) + public comment: string; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public targetUserHost: string | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public reporterHost: string | null; + //#endregion +} diff --git a/packages/backend/src/models/entities/AccessToken.ts b/packages/backend/src/models/entities/AccessToken.ts new file mode 100644 index 0000000000..8e987ffeef --- /dev/null +++ b/packages/backend/src/models/entities/AccessToken.ts @@ -0,0 +1,90 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { App } from './App.js'; + +@Entity() +export class AccessToken { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AccessToken.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + nullable: true, + }) + public lastUsedAt: Date | null; + + @Index() + @Column('varchar', { + length: 128, + }) + public token: string; + + @Index() + @Column('varchar', { + length: 128, + nullable: true, + }) + public session: string | null; + + @Index() + @Column('varchar', { + length: 128, + }) + public hash: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column({ + ...id(), + nullable: true, + }) + public appId: App['id'] | null; + + @ManyToOne(type => App, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public app: App | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public name: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public description: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public iconUrl: string | null; + + @Column('varchar', { + length: 64, array: true, + default: '{}', + }) + public permission: string[]; + + @Column('boolean', { + default: false, + }) + public fetched: boolean; +} diff --git a/packages/backend/src/models/entities/Ad.ts b/packages/backend/src/models/entities/Ad.ts new file mode 100644 index 0000000000..36b758f205 --- /dev/null +++ b/packages/backend/src/models/entities/Ad.ts @@ -0,0 +1,59 @@ +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Ad { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Ad.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + comment: 'The expired date of the Ad.', + }) + public expiresAt: Date; + + @Column('varchar', { + length: 32, nullable: false, + }) + public place: string; + + // 今は使われていないが将来的に活用される可能性はある + @Column('varchar', { + length: 32, nullable: false, + }) + public priority: string; + + @Column('integer', { + default: 1, nullable: false, + }) + public ratio: number; + + @Column('varchar', { + length: 1024, nullable: false, + }) + public url: string; + + @Column('varchar', { + length: 1024, nullable: false, + }) + public imageUrl: string; + + @Column('varchar', { + length: 8192, nullable: false, + }) + public memo: string; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/Announcement.ts b/packages/backend/src/models/entities/Announcement.ts new file mode 100644 index 0000000000..beb2f82462 --- /dev/null +++ b/packages/backend/src/models/entities/Announcement.ts @@ -0,0 +1,43 @@ +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Announcement { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Announcement.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Announcement.', + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 8192, nullable: false, + }) + public text: string; + + @Column('varchar', { + length: 256, nullable: false, + }) + public title: string; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public imageUrl: string | null; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/AnnouncementRead.ts b/packages/backend/src/models/entities/AnnouncementRead.ts new file mode 100644 index 0000000000..72cf688800 --- /dev/null +++ b/packages/backend/src/models/entities/AnnouncementRead.ts @@ -0,0 +1,36 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Announcement } from './Announcement.js'; + +@Entity() +@Index(['userId', 'announcementId'], { unique: true }) +export class AnnouncementRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AnnouncementRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public announcementId: Announcement['id']; + + @ManyToOne(type => Announcement, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public announcement: Announcement | null; +} diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts new file mode 100644 index 0000000000..860fd9cf55 --- /dev/null +++ b/packages/backend/src/models/entities/Antenna.ts @@ -0,0 +1,99 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; +import { UserGroupJoining } from './UserGroupJoining.js'; + +@Entity() +export class Antenna { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Antenna.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Antenna.', + }) + public name: string; + + @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) + public src: 'home' | 'all' | 'users' | 'list' | 'group'; + + @Column({ + ...id(), + nullable: true, + }) + public userListId: UserList['id'] | null; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; + + @Column({ + ...id(), + nullable: true, + }) + public userGroupJoiningId: UserGroupJoining['id'] | null; + + @ManyToOne(type => UserGroupJoining, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userGroupJoining: UserGroupJoining | null; + + @Column('varchar', { + length: 1024, array: true, + default: '{}', + }) + public users: string[]; + + @Column('jsonb', { + default: [], + }) + public keywords: string[][]; + + @Column('jsonb', { + default: [], + }) + public excludeKeywords: string[][]; + + @Column('boolean', { + default: false, + }) + public caseSensitive: boolean; + + @Column('boolean', { + default: false, + }) + public withReplies: boolean; + + @Column('boolean') + public withFile: boolean; + + @Column('varchar', { + length: 2048, nullable: true, + }) + public expression: string | null; + + @Column('boolean') + public notify: boolean; +} diff --git a/packages/backend/src/models/entities/AntennaNote.ts b/packages/backend/src/models/entities/AntennaNote.ts new file mode 100644 index 0000000000..5524a89367 --- /dev/null +++ b/packages/backend/src/models/entities/AntennaNote.ts @@ -0,0 +1,43 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { Antenna } from './Antenna.js'; + +@Entity() +@Index(['noteId', 'antennaId'], { unique: true }) +export class AntennaNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The antenna ID.', + }) + public antennaId: Antenna['id']; + + @ManyToOne(type => Antenna, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public antenna: Antenna | null; + + @Index() + @Column('boolean', { + default: false, + }) + public read: boolean; +} diff --git a/packages/backend/src/models/entities/App.ts b/packages/backend/src/models/entities/App.ts new file mode 100644 index 0000000000..3a1ea7732e --- /dev/null +++ b/packages/backend/src/models/entities/App.ts @@ -0,0 +1,60 @@ +import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class App { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the App.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.', + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + nullable: true, + }) + public user: User | null; + + @Index() + @Column('varchar', { + length: 64, + comment: 'The secret key of the App.', + }) + public secret: string; + + @Column('varchar', { + length: 128, + comment: 'The name of the App.', + }) + public name: string; + + @Column('varchar', { + length: 512, + comment: 'The description of the App.', + }) + public description: string; + + @Column('varchar', { + length: 64, array: true, + comment: 'The permission of the App.', + }) + public permission: string[]; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The callbackUrl of the App.', + }) + public callbackUrl: string | null; +} diff --git a/packages/backend/src/models/entities/AttestationChallenge.ts b/packages/backend/src/models/entities/AttestationChallenge.ts new file mode 100644 index 0000000000..4795642657 --- /dev/null +++ b/packages/backend/src/models/entities/AttestationChallenge.ts @@ -0,0 +1,46 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class AttestationChallenge { + @PrimaryColumn(id()) + public id: string; + + @Index() + @PrimaryColumn(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 64, + comment: 'Hex-encoded sha256 hash of the challenge.', + }) + public challenge: string; + + @Column('timestamp with time zone', { + comment: 'The date challenge was created for expiry purposes.', + }) + public createdAt: Date; + + @Column('boolean', { + comment: + 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.', + default: false, + }) + public registrationChallenge: boolean; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/AuthSession.ts b/packages/backend/src/models/entities/AuthSession.ts new file mode 100644 index 0000000000..6b2f50e8d6 --- /dev/null +++ b/packages/backend/src/models/entities/AuthSession.ts @@ -0,0 +1,43 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { App } from './App.js'; + +@Entity() +export class AuthSession { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AuthSession.', + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 128, + }) + public token: string; + + @Column({ + ...id(), + nullable: true, + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public appId: App['id']; + + @ManyToOne(type => App, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public app: App | null; +} diff --git a/packages/backend/src/models/entities/Blocking.ts b/packages/backend/src/models/entities/Blocking.ts new file mode 100644 index 0000000000..9892ff308e --- /dev/null +++ b/packages/backend/src/models/entities/Blocking.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['blockerId', 'blockeeId'], { unique: true }) +export class Blocking { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Blocking.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The blockee user ID.', + }) + public blockeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public blockee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The blocker user ID.', + }) + public blockerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public blocker: User | null; +} diff --git a/packages/backend/src/models/entities/Channel.ts b/packages/backend/src/models/entities/Channel.ts new file mode 100644 index 0000000000..a6e32d54f7 --- /dev/null +++ b/packages/backend/src/models/entities/Channel.ts @@ -0,0 +1,75 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFile } from './DriveFile.js'; + +@Entity() +export class Channel { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Channel.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public lastNotedAt: Date | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.', + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Channel.', + }) + public name: string; + + @Column('varchar', { + length: 2048, nullable: true, + comment: 'The description of the Channel.', + }) + public description: string | null; + + @Column({ + ...id(), + nullable: true, + comment: 'The ID of banner Channel.', + }) + public bannerId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public banner: DriveFile | null; + + @Index() + @Column('integer', { + default: 0, + comment: 'The count of notes.', + }) + public notesCount: number; + + @Index() + @Column('integer', { + default: 0, + comment: 'The count of users.', + }) + public usersCount: number; +} diff --git a/packages/backend/src/models/entities/ChannelFollowing.ts b/packages/backend/src/models/entities/ChannelFollowing.ts new file mode 100644 index 0000000000..c65c38b67d --- /dev/null +++ b/packages/backend/src/models/entities/ChannelFollowing.ts @@ -0,0 +1,43 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Channel } from './Channel.js'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class ChannelFollowing { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ChannelFollowing.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee channel ID.', + }) + public followeeId: Channel['id']; + + @ManyToOne(type => Channel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public followee: Channel | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.', + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public follower: User | null; +} diff --git a/packages/backend/src/models/entities/ChannelNotePining.ts b/packages/backend/src/models/entities/ChannelNotePining.ts new file mode 100644 index 0000000000..ab5796626a --- /dev/null +++ b/packages/backend/src/models/entities/ChannelNotePining.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { Channel } from './Channel.js'; + +@Entity() +@Index(['channelId', 'noteId'], { unique: true }) +export class ChannelNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the ChannelNotePining.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public channelId: Channel['id']; + + @ManyToOne(type => Channel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public channel: Channel | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/entities/Clip.ts b/packages/backend/src/models/entities/Clip.ts new file mode 100644 index 0000000000..57a310ac03 --- /dev/null +++ b/packages/backend/src/models/entities/Clip.ts @@ -0,0 +1,44 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class Clip { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Clip.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Clip.', + }) + public name: string; + + @Column('boolean', { + default: false, + }) + public isPublic: boolean; + + @Column('varchar', { + length: 2048, nullable: true, + comment: 'The description of the Clip.', + }) + public description: string | null; +} diff --git a/packages/backend/src/models/entities/ClipNote.ts b/packages/backend/src/models/entities/ClipNote.ts new file mode 100644 index 0000000000..bc9ef4b874 --- /dev/null +++ b/packages/backend/src/models/entities/ClipNote.ts @@ -0,0 +1,37 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { Clip } from './Clip.js'; + +@Entity() +@Index(['noteId', 'clipId'], { unique: true }) +export class ClipNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The clip ID.', + }) + public clipId: Clip['id']; + + @ManyToOne(type => Clip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: Clip | null; +} diff --git a/packages/backend/src/models/entities/DriveFile.ts b/packages/backend/src/models/entities/DriveFile.ts new file mode 100644 index 0000000000..7b9670fb92 --- /dev/null +++ b/packages/backend/src/models/entities/DriveFile.ts @@ -0,0 +1,192 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFolder } from './DriveFolder.js'; + +@Entity() +@Index(['userId', 'folderId', 'id']) +export class DriveFile { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFile.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.', + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The host of owner. It will be null if the user in local.', + }) + public userHost: string | null; + + @Index() + @Column('varchar', { + length: 32, + comment: 'The MD5 hash of the DriveFile.', + }) + public md5: string; + + @Column('varchar', { + length: 256, + comment: 'The file name of the DriveFile.', + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, + comment: 'The content type (MIME) of the DriveFile.', + }) + public type: string; + + @Column('integer', { + comment: 'The file size (bytes) of the DriveFile.', + }) + public size: number; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The comment of the DriveFile.', + }) + public comment: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The BlurHash string.', + }) + public blurhash: string | null; + + @Column('jsonb', { + default: {}, + comment: 'The any properties of the DriveFile. For example, it includes image width/height.', + }) + public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; + + @Column('boolean') + public storedInternal: boolean; + + @Column('varchar', { + length: 512, + comment: 'The URL of the DriveFile.', + }) + public url: string; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URL of the thumbnail of the DriveFile.', + }) + public thumbnailUrl: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URL of the webpublic of the DriveFile.', + }) + public webpublicUrl: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public webpublicType: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public accessKey: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public thumbnailAccessKey: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public webpublicAccessKey: string | null; + + @Index() + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.', + }) + public uri: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public src: string | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The parent folder ID. If null, it means the DriveFile is located in root.', + }) + public folderId: DriveFolder['id'] | null; + + @ManyToOne(type => DriveFolder, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public folder: DriveFolder | null; + + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is NSFW.', + }) + public isSensitive: boolean; + + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is NSFW. (predict)', + }) + public maybeSensitive: boolean; + + @Index() + @Column('boolean', { + default: false, + }) + public maybePorn: boolean; + + /** + * 外部の(信頼されていない)URLへの直リンクか否か + */ + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is direct link to remote server.', + }) + public isLink: boolean; + + @Column('jsonb', { + default: {}, + nullable: true, + }) + public requestHeaders: Record | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public requestIp: string | null; +} diff --git a/packages/backend/src/models/entities/DriveFolder.ts b/packages/backend/src/models/entities/DriveFolder.ts new file mode 100644 index 0000000000..2a73a0875d --- /dev/null +++ b/packages/backend/src/models/entities/DriveFolder.ts @@ -0,0 +1,49 @@ +import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class DriveFolder { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFolder.', + }) + public createdAt: Date; + + @Column('varchar', { + length: 128, + comment: 'The name of the DriveFolder.', + }) + public name: string; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.', + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.', + }) + public parentId: DriveFolder['id'] | null; + + @ManyToOne(type => DriveFolder, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public parent: DriveFolder | null; +} diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts new file mode 100644 index 0000000000..7332dd1857 --- /dev/null +++ b/packages/backend/src/models/entities/Emoji.ts @@ -0,0 +1,58 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +@Index(['name', 'host'], { unique: true }) +export class Emoji { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + }) + public updatedAt: Date | null; + + @Index() + @Column('varchar', { + length: 128, + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + }) + public host: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; + + @Column('varchar', { + length: 512, + }) + public originalUrl: string; + + @Column('varchar', { + length: 512, + default: '', + }) + public publicUrl: string; + + @Column('varchar', { + length: 512, nullable: true, + }) + public uri: string | null; + + // publicUrlの方のtypeが入る + @Column('varchar', { + length: 64, nullable: true, + }) + public type: string | null; + + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public aliases: string[]; +} diff --git a/packages/backend/src/models/entities/FollowRequest.ts b/packages/backend/src/models/entities/FollowRequest.ts new file mode 100644 index 0000000000..0988e7e504 --- /dev/null +++ b/packages/backend/src/models/entities/FollowRequest.ts @@ -0,0 +1,85 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class FollowRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the FollowRequest.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.', + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.', + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public follower: User | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'id of Follow Activity.', + }) + public requestId: string | null; + + //#region Denormalized fields + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public followerHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followerInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followerSharedInbox: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public followeeHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followeeInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followeeSharedInbox: string | null; + //#endregion +} diff --git a/packages/backend/src/models/entities/Following.ts b/packages/backend/src/models/entities/Following.ts new file mode 100644 index 0000000000..112afd7e6e --- /dev/null +++ b/packages/backend/src/models/entities/Following.ts @@ -0,0 +1,82 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class Following { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Following.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.', + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.', + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public follower: User | null; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public followerHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followerInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followerSharedInbox: string | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public followeeHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followeeInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]', + }) + public followeeSharedInbox: string | null; + //#endregion +} diff --git a/packages/backend/src/models/entities/GalleryLike.ts b/packages/backend/src/models/entities/GalleryLike.ts new file mode 100644 index 0000000000..cc54b528e9 --- /dev/null +++ b/packages/backend/src/models/entities/GalleryLike.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { GalleryPost } from './GalleryPost.js'; + +@Entity() +@Index(['userId', 'postId'], { unique: true }) +export class GalleryLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public postId: GalleryPost['id']; + + @ManyToOne(type => GalleryPost, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public post: GalleryPost | null; +} diff --git a/packages/backend/src/models/entities/GalleryPost.ts b/packages/backend/src/models/entities/GalleryPost.ts new file mode 100644 index 0000000000..36e879afa7 --- /dev/null +++ b/packages/backend/src/models/entities/GalleryPost.ts @@ -0,0 +1,79 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import type { DriveFile } from './DriveFile.js'; + +@Entity() +export class GalleryPost { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the GalleryPost.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + comment: 'The updated date of the GalleryPost.', + }) + public updatedAt: Date; + + @Column('varchar', { + length: 256, + }) + public title: string; + + @Column('varchar', { + length: 2048, nullable: true, + }) + public description: string | null; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + array: true, default: '{}', + }) + public fileIds: DriveFile['id'][]; + + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the post is sensitive.', + }) + public isSensitive: boolean; + + @Index() + @Column('integer', { + default: 0, + }) + public likedCount: number; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public tags: string[]; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/Hashtag.ts b/packages/backend/src/models/entities/Hashtag.ts new file mode 100644 index 0000000000..2d6bfaa045 --- /dev/null +++ b/packages/backend/src/models/entities/Hashtag.ts @@ -0,0 +1,87 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id.js'; +import type { User } from './User.js'; + +@Entity() +export class Hashtag { + @PrimaryColumn(id()) + public id: string; + + @Index({ unique: true }) + @Column('varchar', { + length: 128, + }) + public name: string; + + @Column({ + ...id(), + array: true, + }) + public mentionedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public mentionedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public mentionedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public mentionedRemoteUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public attachedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public attachedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0, + }) + public attachedRemoteUsersCount: number; +} diff --git a/packages/backend/src/models/entities/Instance.ts b/packages/backend/src/models/entities/Instance.ts new file mode 100644 index 0000000000..7ea9234384 --- /dev/null +++ b/packages/backend/src/models/entities/Instance.ts @@ -0,0 +1,164 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Instance { + @PrimaryColumn(id()) + public id: string; + + /** + * このインスタンスを捕捉した日時 + */ + @Index() + @Column('timestamp with time zone', { + comment: 'The caught date of the Instance.', + }) + public caughtAt: Date; + + /** + * ホスト + */ + @Index({ unique: true }) + @Column('varchar', { + length: 128, + comment: 'The host of the Instance.', + }) + public host: string; + + /** + * インスタンスのユーザー数 + */ + @Column('integer', { + default: 0, + comment: 'The count of the users of the Instance.', + }) + public usersCount: number; + + /** + * インスタンスの投稿数 + */ + @Column('integer', { + default: 0, + comment: 'The count of the notes of the Instance.', + }) + public notesCount: number; + + /** + * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数 + */ + @Column('integer', { + default: 0, + }) + public followingCount: number; + + /** + * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数 + */ + @Column('integer', { + default: 0, + }) + public followersCount: number; + + /** + * 直近のリクエスト送信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestSentAt: Date | null; + + /** + * 直近のリクエスト送信時のHTTPステータスコード + */ + @Column('integer', { + nullable: true, + }) + public latestStatus: number | null; + + /** + * 直近のリクエスト受信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestReceivedAt: Date | null; + + /** + * このインスタンスと最後にやり取りした日時 + */ + @Column('timestamp with time zone') + public lastCommunicatedAt: Date; + + /** + * このインスタンスと不通かどうか + */ + @Column('boolean', { + default: false, + }) + public isNotResponding: boolean; + + /** + * このインスタンスへの配信を停止するか + */ + @Index() + @Column('boolean', { + default: false, + }) + public isSuspended: boolean; + + @Column('varchar', { + length: 64, nullable: true, + comment: 'The software of the Instance.', + }) + public softwareName: string | null; + + @Column('varchar', { + length: 64, nullable: true, + }) + public softwareVersion: string | null; + + @Column('boolean', { + nullable: true, + }) + public openRegistrations: boolean | null; + + @Column('varchar', { + length: 256, nullable: true, + }) + public name: string | null; + + @Column('varchar', { + length: 4096, nullable: true, + }) + public description: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public maintainerName: string | null; + + @Column('varchar', { + length: 256, nullable: true, + }) + public maintainerEmail: string | null; + + @Column('varchar', { + length: 256, nullable: true, + }) + public iconUrl: string | null; + + @Column('varchar', { + length: 256, nullable: true, + }) + public faviconUrl: string | null; + + @Column('varchar', { + length: 64, nullable: true, + }) + public themeColor: string | null; + + @Column('timestamp with time zone', { + nullable: true, + }) + public infoUpdatedAt: Date | null; +} diff --git a/packages/backend/src/models/entities/MessagingMessage.ts b/packages/backend/src/models/entities/MessagingMessage.ts new file mode 100644 index 0000000000..69fc9815d4 --- /dev/null +++ b/packages/backend/src/models/entities/MessagingMessage.ts @@ -0,0 +1,89 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFile } from './DriveFile.js'; +import { UserGroup } from './UserGroup.js'; + +@Entity() +export class MessagingMessage { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the MessagingMessage.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The sender user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), nullable: true, + comment: 'The recipient user ID.', + }) + public recipientId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public recipient: User | null; + + @Index() + @Column({ + ...id(), nullable: true, + comment: 'The recipient group ID.', + }) + public groupId: UserGroup['id'] | null; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public group: UserGroup | null; + + @Column('varchar', { + length: 4096, nullable: true, + }) + public text: string | null; + + @Column('boolean', { + default: false, + }) + public isRead: boolean; + + @Column('varchar', { + length: 512, nullable: true, + }) + public uri: string | null; + + @Column({ + ...id(), + array: true, default: '{}', + }) + public reads: User['id'][]; + + @Column({ + ...id(), + nullable: true, + }) + public fileId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public file: DriveFile | null; +} diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts new file mode 100644 index 0000000000..f528b7ac08 --- /dev/null +++ b/packages/backend/src/models/entities/Meta.ts @@ -0,0 +1,462 @@ +import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import type { Clip } from './Clip.js'; + +@Entity() +export class Meta { + @PrimaryColumn({ + type: 'varchar', + length: 32, + }) + public id: string; + + @Column('varchar', { + length: 128, nullable: true, + }) + public name: string | null; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public description: string | null; + + /** + * メンテナの名前 + */ + @Column('varchar', { + length: 128, nullable: true, + }) + public maintainerName: string | null; + + /** + * メンテナの連絡先 + */ + @Column('varchar', { + length: 128, nullable: true, + }) + public maintainerEmail: string | null; + + @Column('boolean', { + default: false, + }) + public disableRegistration: boolean; + + @Column('boolean', { + default: false, + }) + public disableLocalTimeline: boolean; + + @Column('boolean', { + default: false, + }) + public disableGlobalTimeline: boolean; + + @Column('boolean', { + default: false, + }) + public useStarForReactionFallback: boolean; + + @Column('varchar', { + length: 64, array: true, default: '{}', + }) + public langs: string[]; + + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public pinnedUsers: string[]; + + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public hiddenTags: string[]; + + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public blockedHosts: string[]; + + @Column('varchar', { + length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', + }) + public pinnedPages: string[]; + + @Column({ + ...id(), + nullable: true, + }) + public pinnedClipId: Clip['id'] | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public themeColor: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + default: '/assets/ai.png', + }) + public mascotImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public bannerUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public backgroundImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public logoImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + default: 'https://xn--931a.moe/aiart/yubitun.png', + }) + public errorImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public iconUrl: string | null; + + @Column('boolean', { + default: true, + }) + public cacheRemoteFiles: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public proxyAccountId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public proxyAccount: User | null; + + @Column('boolean', { + default: false, + }) + public emailRequiredForSignup: boolean; + + @Column('boolean', { + default: false, + }) + public enableHcaptcha: boolean; + + @Column('varchar', { + length: 64, + nullable: true, + }) + public hcaptchaSiteKey: string | null; + + @Column('varchar', { + length: 64, + nullable: true, + }) + public hcaptchaSecretKey: string | null; + + @Column('boolean', { + default: false, + }) + public enableRecaptcha: boolean; + + @Column('varchar', { + length: 64, + nullable: true, + }) + public recaptchaSiteKey: string | null; + + @Column('varchar', { + length: 64, + nullable: true, + }) + public recaptchaSecretKey: string | null; + + @Column('enum', { + enum: ['none', 'all', 'local', 'remote'], + default: 'none', + }) + public sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote'; + + @Column('enum', { + enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'], + default: 'medium', + }) + public sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh'; + + @Column('boolean', { + default: false, + }) + public setSensitiveFlagAutomatically: boolean; + + @Column('boolean', { + default: false, + }) + 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)', + }) + public remoteDriveCapacityMb: number; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public summalyProxy: string | null; + + @Column('boolean', { + default: false, + }) + public enableEmail: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public email: string | null; + + @Column('boolean', { + default: false, + }) + public smtpSecure: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public smtpHost: string | null; + + @Column('integer', { + nullable: true, + }) + public smtpPort: number | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public smtpUser: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public smtpPass: string | null; + + @Column('boolean', { + default: false, + }) + public enableServiceWorker: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public swPublicKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public swPrivateKey: string | null; + + @Column('boolean', { + default: false, + }) + public enableTwitterIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public twitterConsumerKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public twitterConsumerSecret: string | null; + + @Column('boolean', { + default: false, + }) + public enableGithubIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public githubClientId: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public githubClientSecret: string | null; + + @Column('boolean', { + default: false, + }) + public enableDiscordIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public discordClientId: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public discordClientSecret: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public deeplAuthKey: string | null; + + @Column('boolean', { + default: false, + }) + public deeplIsPro: boolean; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public ToSUrl: string | null; + + @Column('varchar', { + length: 512, + default: 'https://github.com/misskey-dev/misskey', + nullable: false, + }) + public repositoryUrl: string; + + @Column('varchar', { + length: 512, + default: 'https://github.com/misskey-dev/misskey/issues/new', + nullable: true, + }) + public feedbackUrl: string | null; + + @Column('varchar', { + length: 8192, + nullable: true, + }) + public defaultLightTheme: string | null; + + @Column('varchar', { + length: 8192, + nullable: true, + }) + public defaultDarkTheme: string | null; + + @Column('boolean', { + default: false, + }) + public useObjectStorage: boolean; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageBucket: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStoragePrefix: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageBaseUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageEndpoint: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageRegion: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageAccessKey: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + }) + public objectStorageSecretKey: string | null; + + @Column('integer', { + nullable: true, + }) + public objectStoragePort: number | null; + + @Column('boolean', { + default: true, + }) + public objectStorageUseSSL: boolean; + + @Column('boolean', { + default: true, + }) + public objectStorageUseProxy: boolean; + + @Column('boolean', { + default: false, + }) + public objectStorageSetPublicRead: boolean; + + @Column('boolean', { + default: true, + }) + public objectStorageS3ForcePathStyle: boolean; + + @Column('boolean', { + default: false, + }) + public enableIpLogging: boolean; + + @Column('boolean', { + default: true, + }) + public enableActiveEmailValidation: boolean; +} diff --git a/packages/backend/src/models/entities/ModerationLog.ts b/packages/backend/src/models/entities/ModerationLog.ts new file mode 100644 index 0000000000..ab6a226cf7 --- /dev/null +++ b/packages/backend/src/models/entities/ModerationLog.ts @@ -0,0 +1,32 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class ModerationLog { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the ModerationLog.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + }) + public type: string; + + @Column('jsonb') + public info: Record; +} diff --git a/packages/backend/src/models/entities/MutedNote.ts b/packages/backend/src/models/entities/MutedNote.ts new file mode 100644 index 0000000000..78347d8917 --- /dev/null +++ b/packages/backend/src/models/entities/MutedNote.ts @@ -0,0 +1,48 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { mutedNoteReasons } from '../../types.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['noteId', 'userId'], { unique: true }) +export class MutedNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + /** + * ミュートされた理由。 + */ + @Index() + @Column('enum', { + enum: mutedNoteReasons, + comment: 'The reason of the MutedNote.', + }) + public reason: typeof mutedNoteReasons[number]; +} diff --git a/packages/backend/src/models/entities/Muting.ts b/packages/backend/src/models/entities/Muting.ts new file mode 100644 index 0000000000..bf5498b96a --- /dev/null +++ b/packages/backend/src/models/entities/Muting.ts @@ -0,0 +1,48 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['muterId', 'muteeId'], { unique: true }) +export class Muting { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date | null; + + @Index() + @Column({ + ...id(), + comment: 'The mutee user ID.', + }) + public muteeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public mutee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The muter user ID.', + }) + public muterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public muter: User | null; +} diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/entities/Note.ts new file mode 100644 index 0000000000..f1a94bd9ce --- /dev/null +++ b/packages/backend/src/models/entities/Note.ts @@ -0,0 +1,242 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { noteVisibilities } from '../../types.js'; +import { User } from './User.js'; +import { Channel } from './Channel.js'; +import type { DriveFile } from './DriveFile.js'; + +@Entity() +@Index('IDX_NOTE_TAGS', { synchronize: false }) +@Index('IDX_NOTE_MENTIONS', { synchronize: false }) +@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) +export class Note { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Note.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of reply target.', + }) + public replyId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public reply: Note | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of renote target.', + }) + public renoteId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public renote: Note | null; + + @Index() + @Column('varchar', { + length: 256, nullable: true, + }) + public threadId: string | null; + + @Column('text', { + nullable: true, + }) + public text: string | null; + + @Column('varchar', { + length: 256, nullable: true, + }) + public name: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public cw: string | null; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('smallint', { + default: 0, + }) + public renoteCount: number; + + @Column('smallint', { + default: 0, + }) + public repliesCount: number; + + @Column('jsonb', { + default: {}, + }) + public reactions: Record; + + /** + * public ... 公開 + * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す + * followers ... フォロワーのみ + * specified ... visibleUserIds で指定したユーザーのみ + */ + @Column('enum', { enum: noteVisibilities }) + public visibility: typeof noteVisibilities[number]; + + @Index({ unique: true }) + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of a note. it will be null when the note is local.', + }) + public uri: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The human readable url of a note. it will be null when the note is local.', + }) + public url: string | null; + + @Column('integer', { + default: 0, select: false, + }) + public score: number; + + @Index() + @Column({ + ...id(), + array: true, default: '{}', + }) + public fileIds: DriveFile['id'][]; + + @Index() + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public attachedFileTypes: string[]; + + @Index() + @Column({ + ...id(), + array: true, default: '{}', + }) + public visibleUserIds: User['id'][]; + + @Index() + @Column({ + ...id(), + array: true, default: '{}', + }) + public mentions: User['id'][]; + + @Column('text', { + default: '[]', + }) + public mentionedRemoteUsers: string; + + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public emojis: string[]; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public tags: string[]; + + @Column('boolean', { + default: false, + }) + public hasPoll: boolean; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of source channel.', + }) + public channelId: Channel['id'] | null; + + @ManyToOne(type => Channel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public channel: Channel | null; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public userHost: string | null; + + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]', + }) + public replyUserId: User['id'] | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public replyUserHost: string | null; + + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]', + }) + public renoteUserId: User['id'] | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public renoteUserHost: string | null; + //#endregion + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export type IMentionedRemoteUsers = { + uri: string; + url?: string; + username: string; + host: string; +}[]; diff --git a/packages/backend/src/models/entities/NoteFavorite.ts b/packages/backend/src/models/entities/NoteFavorite.ts new file mode 100644 index 0000000000..80c97cb531 --- /dev/null +++ b/packages/backend/src/models/entities/NoteFavorite.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the NoteFavorite.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/entities/NoteReaction.ts b/packages/backend/src/models/entities/NoteReaction.ts new file mode 100644 index 0000000000..c3c381af56 --- /dev/null +++ b/packages/backend/src/models/entities/NoteReaction.ts @@ -0,0 +1,44 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteReaction { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the NoteReaction.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user?: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note?: Note | null; + + // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) + + @Column('varchar', { + length: 260, + }) + public reaction: string; +} diff --git a/packages/backend/src/models/entities/NoteThreadMuting.ts b/packages/backend/src/models/entities/NoteThreadMuting.ts new file mode 100644 index 0000000000..a23176b994 --- /dev/null +++ b/packages/backend/src/models/entities/NoteThreadMuting.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; + +@Entity() +@Index(['userId', 'threadId'], { unique: true }) +export class NoteThreadMuting { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 256, + }) + public threadId: string; +} diff --git a/packages/backend/src/models/entities/NoteUnread.ts b/packages/backend/src/models/entities/NoteUnread.ts new file mode 100644 index 0000000000..af91234d0f --- /dev/null +++ b/packages/backend/src/models/entities/NoteUnread.ts @@ -0,0 +1,63 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; +import type { Channel } from './Channel.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteUnread { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + /** + * メンションか否か + */ + @Index() + @Column('boolean') + public isMentioned: boolean; + + /** + * ダイレクト投稿か否か + */ + @Index() + @Column('boolean') + public isSpecified: boolean; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public noteUserId: User['id']; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]', + }) + public noteChannelId: Channel['id'] | null; + //#endregion +} diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts new file mode 100644 index 0000000000..53a7dda43a --- /dev/null +++ b/packages/backend/src/models/entities/Notification.ts @@ -0,0 +1,173 @@ +import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; +import { notificationTypes } from '@/types.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; +import { FollowRequest } from './FollowRequest.js'; +import { UserGroupInvitation } from './UserGroupInvitation.js'; +import { AccessToken } from './AccessToken.js'; + +@Entity() +export class Notification { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Notification.', + }) + public createdAt: Date; + + /** + * 通知の受信者 + */ + @Index() + @Column({ + ...id(), + comment: 'The ID of recipient user of the Notification.', + }) + public notifieeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public notifiee: User | null; + + /** + * 通知の送信者(initiator) + */ + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of sender user of the Notification.', + }) + public notifierId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public notifier: User | null; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - (自分または自分がWatchしている)投稿が返信された + * renote - (自分または自分がWatchしている)投稿がRenoteされた + * quote - (自分または自分がWatchしている)投稿が引用Renoteされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * receiveFollowRequest - フォローリクエストされた + * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * groupInvited - グループに招待された + * app - アプリ通知 + */ + @Index() + @Column('enum', { + enum: notificationTypes, + comment: 'The type of the Notification.', + }) + public type: typeof notificationTypes[number]; + + /** + * 通知が読まれたかどうか + */ + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the Notification is read.', + }) + public isRead: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public noteId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column({ + ...id(), + nullable: true, + }) + public followRequestId: FollowRequest['id'] | null; + + @ManyToOne(type => FollowRequest, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public followRequest: FollowRequest | null; + + @Column({ + ...id(), + nullable: true, + }) + public userGroupInvitationId: UserGroupInvitation['id'] | null; + + @ManyToOne(type => UserGroupInvitation, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userGroupInvitation: UserGroupInvitation | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public reaction: string | null; + + @Column('integer', { + nullable: true, + }) + public choice: number | null; + + /** + * アプリ通知のbody + */ + @Column('varchar', { + length: 2048, nullable: true, + }) + public customBody: string | null; + + /** + * アプリ通知のheader + * (省略時はアプリ名で表示されることを期待) + */ + @Column('varchar', { + length: 256, nullable: true, + }) + public customHeader: string | null; + + /** + * アプリ通知のicon(URL) + * (省略時はアプリアイコンで表示されることを期待) + */ + @Column('varchar', { + length: 1024, nullable: true, + }) + public customIcon: string | null; + + /** + * アプリ通知のアプリ(のトークン) + */ + @Index() + @Column({ + ...id(), + nullable: true, + }) + public appAccessTokenId: AccessToken['id'] | null; + + @ManyToOne(type => AccessToken, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public appAccessToken: AccessToken | null; +} diff --git a/packages/backend/src/models/entities/Page.ts b/packages/backend/src/models/entities/Page.ts new file mode 100644 index 0000000000..6078bc1bc7 --- /dev/null +++ b/packages/backend/src/models/entities/Page.ts @@ -0,0 +1,121 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFile } from './DriveFile.js'; + +@Entity() +@Index(['userId', 'name'], { unique: true }) +export class Page { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Page.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + comment: 'The updated date of the Page.', + }) + public updatedAt: Date; + + @Column('varchar', { + length: 256, + }) + public title: string; + + @Index() + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 256, nullable: true, + }) + public summary: string | null; + + @Column('boolean') + public alignCenter: boolean; + + @Column('boolean', { + default: false, + }) + public hideTitleWhenPinned: boolean; + + @Column('varchar', { + length: 32, + }) + public font: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column({ + ...id(), + nullable: true, + }) + public eyeCatchingImageId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public eyeCatchingImage: DriveFile | null; + + @Column('jsonb', { + default: [], + }) + public content: Record[]; + + @Column('jsonb', { + default: [], + }) + public variables: Record[]; + + @Column('varchar', { + length: 16384, + default: '', + }) + public script: string; + + /** + * public ... 公開 + * followers ... フォロワーのみ + * specified ... visibleUserIds で指定したユーザーのみ + */ + @Column('enum', { enum: ['public', 'followers', 'specified'] }) + public visibility: 'public' | 'followers' | 'specified'; + + @Index() + @Column({ + ...id(), + array: true, default: '{}', + }) + public visibleUserIds: User['id'][]; + + @Column('integer', { + default: 0, + }) + public likedCount: number; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/PageLike.ts b/packages/backend/src/models/entities/PageLike.ts new file mode 100644 index 0000000000..f8c5943a3e --- /dev/null +++ b/packages/backend/src/models/entities/PageLike.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Page } from './Page.js'; + +@Entity() +@Index(['userId', 'pageId'], { unique: true }) +export class PageLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public pageId: Page['id']; + + @ManyToOne(type => Page, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public page: Page | null; +} diff --git a/packages/backend/src/models/entities/PasswordResetRequest.ts b/packages/backend/src/models/entities/PasswordResetRequest.ts new file mode 100644 index 0000000000..939fcc460f --- /dev/null +++ b/packages/backend/src/models/entities/PasswordResetRequest.ts @@ -0,0 +1,30 @@ +import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class PasswordResetRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, + }) + public token: string; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; +} diff --git a/packages/backend/src/models/entities/Poll.ts b/packages/backend/src/models/entities/Poll.ts new file mode 100644 index 0000000000..6641b435eb --- /dev/null +++ b/packages/backend/src/models/entities/Poll.ts @@ -0,0 +1,72 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id.js'; +import { noteVisibilities } from '../../types.js'; +import { Note } from './Note.js'; +import type { User } from './User.js'; + +@Entity() +export class Poll { + @PrimaryColumn(id()) + public noteId: Note['id']; + + @OneToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date | null; + + @Column('boolean') + public multiple: boolean; + + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public choices: string[]; + + @Column('integer', { + array: true, + }) + public votes: number[]; + + //#region Denormalized fields + @Column('enum', { + enum: noteVisibilities, + comment: '[Denormalized]', + }) + public noteVisibility: typeof noteVisibilities[number]; + + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public userId: User['id']; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public userHost: string | null; + //#endregion + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export type IPoll = { + choices: string[]; + votes?: number[]; + multiple: boolean; + expiresAt: Date | null; +}; diff --git a/packages/backend/src/models/entities/PollVote.ts b/packages/backend/src/models/entities/PollVote.ts new file mode 100644 index 0000000000..d447a7be8f --- /dev/null +++ b/packages/backend/src/models/entities/PollVote.ts @@ -0,0 +1,40 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; + +@Entity() +@Index(['userId', 'noteId', 'choice'], { unique: true }) +export class PollVote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the PollVote.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column('integer') + public choice: number; +} diff --git a/packages/backend/src/models/entities/PromoNote.ts b/packages/backend/src/models/entities/PromoNote.ts new file mode 100644 index 0000000000..958008338a --- /dev/null +++ b/packages/backend/src/models/entities/PromoNote.ts @@ -0,0 +1,28 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import type { User } from './User.js'; + +@Entity() +export class PromoNote { + @PrimaryColumn(id()) + public noteId: Note['id']; + + @OneToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column('timestamp with time zone') + public expiresAt: Date; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public userId: User['id']; + //#endregion +} diff --git a/packages/backend/src/models/entities/PromoRead.ts b/packages/backend/src/models/entities/PromoRead.ts new file mode 100644 index 0000000000..27f5d0dc11 --- /dev/null +++ b/packages/backend/src/models/entities/PromoRead.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class PromoRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the PromoRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/entities/RegistrationTickets.ts b/packages/backend/src/models/entities/RegistrationTickets.ts new file mode 100644 index 0000000000..139e40f85e --- /dev/null +++ b/packages/backend/src/models/entities/RegistrationTickets.ts @@ -0,0 +1,17 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class RegistrationTicket { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 64, + }) + public code: string; +} diff --git a/packages/backend/src/models/entities/RegistryItem.ts b/packages/backend/src/models/entities/RegistryItem.ts new file mode 100644 index 0000000000..670a236ea0 --- /dev/null +++ b/packages/backend/src/models/entities/RegistryItem.ts @@ -0,0 +1,58 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい +@Entity() +export class RegistryItem { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the RegistryItem.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the RegistryItem.', + }) + public updatedAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 1024, + comment: 'The key of the RegistryItem.', + }) + public key: string; + + @Column('jsonb', { + default: {}, nullable: true, + comment: 'The value of the RegistryItem.', + }) + public value: any | null; + + @Index() + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public scope: string[]; + + // サードパーティアプリに開放するときのためのカラム + @Index() + @Column('varchar', { + length: 512, nullable: true, + }) + public domain: string | null; +} diff --git a/packages/backend/src/models/entities/Relay.ts b/packages/backend/src/models/entities/Relay.ts new file mode 100644 index 0000000000..94d1929574 --- /dev/null +++ b/packages/backend/src/models/entities/Relay.ts @@ -0,0 +1,19 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Relay { + @PrimaryColumn(id()) + public id: string; + + @Index({ unique: true }) + @Column('varchar', { + length: 512, nullable: false, + }) + public inbox: string; + + @Column('enum', { + enum: ['requesting', 'accepted', 'rejected'], + }) + public status: 'requesting' | 'accepted' | 'rejected'; +} diff --git a/packages/backend/src/models/entities/Signin.ts b/packages/backend/src/models/entities/Signin.ts new file mode 100644 index 0000000000..380bf028a6 --- /dev/null +++ b/packages/backend/src/models/entities/Signin.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class Signin { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Signin.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + }) + public ip: string; + + @Column('jsonb') + public headers: Record; + + @Column('boolean') + public success: boolean; +} diff --git a/packages/backend/src/models/entities/SwSubscription.ts b/packages/backend/src/models/entities/SwSubscription.ts new file mode 100644 index 0000000000..51b9786e96 --- /dev/null +++ b/packages/backend/src/models/entities/SwSubscription.ts @@ -0,0 +1,37 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class SwSubscription { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 512, + }) + public endpoint: string; + + @Column('varchar', { + length: 256, + }) + public auth: string; + + @Column('varchar', { + length: 128, + }) + public publickey: string; +} diff --git a/packages/backend/src/models/entities/UsedUsername.ts b/packages/backend/src/models/entities/UsedUsername.ts new file mode 100644 index 0000000000..eb90bef6ca --- /dev/null +++ b/packages/backend/src/models/entities/UsedUsername.ts @@ -0,0 +1,20 @@ +import { PrimaryColumn, Entity, Column } from 'typeorm'; + +@Entity() +export class UsedUsername { + @PrimaryColumn('varchar', { + length: 128, + }) + public username: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts new file mode 100644 index 0000000000..73736f0150 --- /dev/null +++ b/packages/backend/src/models/entities/User.ts @@ -0,0 +1,255 @@ +import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { DriveFile } from './DriveFile.js'; + +@Entity() +@Index(['usernameLower', 'host'], { unique: true }) +export class User { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the User.', + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + comment: 'The updated date of the User.', + }) + public updatedAt: Date | null; + + @Column('timestamp with time zone', { + nullable: true, + }) + public lastFetchedAt: Date | null; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public lastActiveDate: Date | null; + + @Column('boolean', { + default: false, + }) + public hideOnlineStatus: boolean; + + @Column('varchar', { + length: 128, + comment: 'The username of the User.', + }) + public username: string; + + @Index() + @Column('varchar', { + length: 128, select: false, + comment: 'The username (lowercased) of the User.', + }) + public usernameLower: string; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The name of the User.', + }) + public name: string | null; + + @Column('integer', { + default: 0, + comment: 'The count of followers.', + }) + public followersCount: number; + + @Column('integer', { + default: 0, + comment: 'The count of following.', + }) + public followingCount: number; + + @Column('integer', { + default: 0, + comment: 'The count of notes.', + }) + public notesCount: number; + + @Column({ + ...id(), + nullable: true, + comment: 'The ID of avatar DriveFile.', + }) + public avatarId: DriveFile['id'] | null; + + @OneToOne(type => DriveFile, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public avatar: DriveFile | null; + + @Column({ + ...id(), + nullable: true, + comment: 'The ID of banner DriveFile.', + }) + public bannerId: DriveFile['id'] | null; + + @OneToOne(type => DriveFile, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public banner: DriveFile | null; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public tags: string[]; + + @Column('boolean', { + default: false, + comment: 'Whether the User is suspended.', + }) + 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.', + }) + public isLocked: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is a bot.', + }) + public isBot: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is a cat.', + }) + public isCat: boolean; + + @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.', + }) + public isModerator: boolean; + + @Index() + @Column('boolean', { + default: true, + comment: 'Whether the User is explorable.', + }) + public isExplorable: boolean; + + // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ + @Column('boolean', { + default: false, + comment: 'Whether the User is deleted.', + }) + public isDeleted: boolean; + + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public emojis: string[]; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The host of the User. It will be null if the origin of the user is local.', + }) + public host: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The inbox URL of the User. It will be null if the origin of the user is local.', + }) + public inbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.', + }) + public sharedInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The featured URL of the User. It will be null if the origin of the user is local.', + }) + public featured: string | null; + + @Index() + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of the User. It will be null if the origin of the user is local.', + }) + public uri: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.', + }) + public followersUri: string | null; + + @Column('boolean', { + default: false, + comment: 'Whether to show users replying to other users in the timeline.', + }) + public showTimelineReplies: boolean; + + @Index({ unique: true }) + @Column('char', { + length: 16, nullable: true, unique: true, + comment: 'The native access token of the User. It will be null if the origin of the user is local.', + }) + 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; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export interface ILocalUser extends User { + host: null; +} + +export interface IRemoteUser extends User { + host: string; +} + +export type CacheableLocalUser = ILocalUser; + +export type CacheableRemoteUser = IRemoteUser; + +export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; + +export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; +export const passwordSchema = { type: 'string', minLength: 1 } as const; +export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; +export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const; +export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; +export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/entities/UserGroup.ts b/packages/backend/src/models/entities/UserGroup.ts new file mode 100644 index 0000000000..328a1883cb --- /dev/null +++ b/packages/backend/src/models/entities/UserGroup.ts @@ -0,0 +1,46 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserGroup { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroup.', + }) + public createdAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of owner.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('boolean', { + default: false, + }) + public isPrivate: boolean; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/UserGroupInvitation.ts b/packages/backend/src/models/entities/UserGroupInvitation.ts new file mode 100644 index 0000000000..e4aa3ccae1 --- /dev/null +++ b/packages/backend/src/models/entities/UserGroupInvitation.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserGroup } from './UserGroup.js'; + +@Entity() +@Index(['userId', 'userGroupId'], { unique: true }) +export class UserGroupInvitation { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroupInvitation.', + }) + 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 group ID.', + }) + public userGroupId: UserGroup['id']; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userGroup: UserGroup | null; +} diff --git a/packages/backend/src/models/entities/UserGroupJoining.ts b/packages/backend/src/models/entities/UserGroupJoining.ts new file mode 100644 index 0000000000..fae7241525 --- /dev/null +++ b/packages/backend/src/models/entities/UserGroupJoining.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserGroup } from './UserGroup.js'; + +@Entity() +@Index(['userId', 'userGroupId'], { unique: true }) +export class UserGroupJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroupJoining.', + }) + 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 group ID.', + }) + public userGroupId: UserGroup['id']; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userGroup: UserGroup | null; +} diff --git a/packages/backend/src/models/entities/UserIp.ts b/packages/backend/src/models/entities/UserIp.ts new file mode 100644 index 0000000000..e9afd40d49 --- /dev/null +++ b/packages/backend/src/models/entities/UserIp.ts @@ -0,0 +1,24 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import type { User } from './User.js'; + +@Entity() +@Index(['userId', 'ip'], { unique: true }) +export class UserIp { + @PrimaryGeneratedColumn() + public id: string; + + @Column('timestamp with time zone', { + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @Column('varchar', { + length: 128, + }) + public ip: string; +} diff --git a/packages/backend/src/models/entities/UserKeypair.ts b/packages/backend/src/models/entities/UserKeypair.ts new file mode 100644 index 0000000000..3cd02d3c4f --- /dev/null +++ b/packages/backend/src/models/entities/UserKeypair.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserKeypair { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 4096, + }) + public publicKey: string; + + @Column('varchar', { + length: 4096, + }) + public privateKey: string; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/UserList.ts b/packages/backend/src/models/entities/UserList.ts new file mode 100644 index 0000000000..b8a4b54d4c --- /dev/null +++ b/packages/backend/src/models/entities/UserList.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserList { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserList.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the UserList.', + }) + public name: string; +} diff --git a/packages/backend/src/models/entities/UserListJoining.ts b/packages/backend/src/models/entities/UserListJoining.ts new file mode 100644 index 0000000000..a40793a3e8 --- /dev/null +++ b/packages/backend/src/models/entities/UserListJoining.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; + +@Entity() +@Index(['userId', 'userListId'], { unique: true }) +export class UserListJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserListJoining.', + }) + 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 list ID.', + }) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/packages/backend/src/models/entities/UserNotePining.ts b/packages/backend/src/models/entities/UserNotePining.ts new file mode 100644 index 0000000000..fee95d4f7d --- /dev/null +++ b/packages/backend/src/models/entities/UserNotePining.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class UserNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserNotePinings.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/entities/UserPending.ts b/packages/backend/src/models/entities/UserPending.ts new file mode 100644 index 0000000000..7637948841 --- /dev/null +++ b/packages/backend/src/models/entities/UserPending.ts @@ -0,0 +1,32 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class UserPending { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 128, + }) + public code: string; + + @Column('varchar', { + length: 128, + }) + public username: string; + + @Column('varchar', { + length: 128, + }) + public email: string; + + @Column('varchar', { + length: 128, + }) + public password: string; +} diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts new file mode 100644 index 0000000000..c561da87ce --- /dev/null +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -0,0 +1,232 @@ +import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { ffVisibility, notificationTypes } from '@/types.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Page } from './Page.js'; + +// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも +// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン +@Entity() +export class UserProfile { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The location of the User.', + }) + public location: string | null; + + @Column('char', { + length: 10, nullable: true, + comment: 'The birthday (YYYY-MM-DD) of the User.', + }) + public birthday: string | null; + + @Column('varchar', { + length: 2048, nullable: true, + comment: 'The description (bio) of the User.', + }) + public description: string | null; + + @Column('jsonb', { + default: [], + }) + public fields: { + name: string; + value: string; + }[]; + + @Column('varchar', { + length: 32, nullable: true, + }) + public lang: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'Remote URL of the user.', + }) + public url: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The email address of the User.', + }) + public email: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public emailVerifyCode: string | null; + + @Column('boolean', { + default: false, + }) + public emailVerified: boolean; + + @Column('jsonb', { + default: ['follow', 'receiveFollowRequest', 'groupInvited'], + }) + public emailNotificationTypes: string[]; + + @Column('boolean', { + default: false, + }) + public publicReactions: boolean; + + @Column('enum', { + enum: ffVisibility, + default: 'public', + }) + public ffVisibility: typeof ffVisibility[number]; + + @Column('varchar', { + length: 128, nullable: true, + }) + public twoFactorTempSecret: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public twoFactorSecret: string | null; + + @Column('boolean', { + default: false, + }) + public twoFactorEnabled: boolean; + + @Column('boolean', { + default: false, + }) + public securityKeysAvailable: boolean; + + @Column('boolean', { + default: false, + }) + public usePasswordLessLogin: boolean; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The password hash of the User. It will be null if the origin of the user is local.', + }) + public password: string | null; + + @Column('varchar', { + length: 8192, default: '', + }) + public moderationNote: string | null; + + // TODO: そのうち消す + @Column('jsonb', { + default: {}, + comment: 'The client-specific data of the User.', + }) + public clientData: Record; + + // TODO: そのうち消す + @Column('jsonb', { + default: {}, + comment: 'The room data of the User.', + }) + public room: Record; + + @Column('boolean', { + default: false, + }) + public autoAcceptFollowed: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether reject index by crawler.', + }) + public noCrawle: boolean; + + @Column('boolean', { + default: false, + }) + public alwaysMarkNsfw: boolean; + + @Column('boolean', { + default: false, + }) + public autoSensitive: boolean; + + @Column('boolean', { + default: false, + }) + public carefulBot: boolean; + + @Column('boolean', { + default: true, + }) + public injectFeaturedNote: boolean; + + @Column('boolean', { + default: true, + }) + public receiveAnnouncementEmail: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public pinnedPageId: Page['id'] | null; + + @OneToOne(type => Page, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public pinnedPage: Page | null; + + @Column('jsonb', { + default: {}, + }) + public integrations: Record; + + @Index() + @Column('boolean', { + default: false, select: false, + }) + public enableWordMute: boolean; + + @Column('jsonb', { + default: [], + }) + public mutedWords: string[][]; + + @Column('jsonb', { + default: [], + comment: 'List of instances muted by the user.', + }) + public mutedInstances: string[]; + + @Column('enum', { + enum: notificationTypes, + array: true, + default: [], + }) + public mutingNotificationTypes: typeof notificationTypes[number][]; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public userHost: string | null; + //#endregion + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/UserPublickey.ts b/packages/backend/src/models/entities/UserPublickey.ts new file mode 100644 index 0000000000..7b505e5b4c --- /dev/null +++ b/packages/backend/src/models/entities/UserPublickey.ts @@ -0,0 +1,34 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserPublickey { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, + }) + public keyId: string; + + @Column('varchar', { + length: 4096, + }) + public keyPem: string; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/UserSecurityKey.ts b/packages/backend/src/models/entities/UserSecurityKey.ts new file mode 100644 index 0000000000..947692a32b --- /dev/null +++ b/packages/backend/src/models/entities/UserSecurityKey.ts @@ -0,0 +1,48 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserSecurityKey { + @PrimaryColumn('varchar', { + comment: 'Variable-length id given to navigator.credentials.get()', + }) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + comment: + 'Variable-length public key used to verify attestations (hex-encoded).', + }) + public publicKey: string; + + @Column('timestamp with time zone', { + comment: + 'The date of the last time the UserSecurityKey was successfully validated.', + }) + public lastUsed: Date; + + @Column('varchar', { + comment: 'User-defined name for this key', + length: 30, + }) + public name: string; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/Webhook.ts b/packages/backend/src/models/entities/Webhook.ts new file mode 100644 index 0000000000..eabb604de9 --- /dev/null +++ b/packages/backend/src/models/entities/Webhook.ts @@ -0,0 +1,73 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; + +@Entity() +export class Webhook { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Antenna.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Antenna.', + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public on: (typeof webhookEventTypes)[number][]; + + @Column('varchar', { + length: 1024, + }) + public url: string; + + @Column('varchar', { + length: 1024, + }) + public secret: string; + + @Index() + @Column('boolean', { + default: true, + }) + public active: boolean; + + /** + * 直近のリクエスト送信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestSentAt: Date | null; + + /** + * 直近のリクエスト送信時のHTTPステータスコード + */ + @Column('integer', { + nullable: true, + }) + public latestStatus: number | null; +} diff --git a/packages/backend/src/models/entities/abuse-user-report.ts b/packages/backend/src/models/entities/abuse-user-report.ts deleted file mode 100644 index 6ac5635528..0000000000 --- a/packages/backend/src/models/entities/abuse-user-report.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class AbuseUserReport { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the AbuseUserReport.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public targetUserId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public targetUser: User | null; - - @Index() - @Column(id()) - public reporterId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public reporter: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public assigneeId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public assignee: User | null; - - @Index() - @Column('boolean', { - default: false, - }) - public resolved: boolean; - - @Column('boolean', { - default: false - }) - public forwarded: boolean; - - @Column('varchar', { - length: 2048, - }) - public comment: string; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public targetUserHost: string | null; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public reporterHost: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts deleted file mode 100644 index c6e2141a46..0000000000 --- a/packages/backend/src/models/entities/access-token.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.js'; -import { App } from './app.js'; -import { id } from '../id.js'; - -@Entity() -export class AccessToken { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the AccessToken.', - }) - public createdAt: Date; - - @Column('timestamp with time zone', { - nullable: true, - }) - public lastUsedAt: Date | null; - - @Index() - @Column('varchar', { - length: 128, - }) - public token: string; - - @Index() - @Column('varchar', { - length: 128, - nullable: true, - }) - public session: string | null; - - @Index() - @Column('varchar', { - length: 128, - }) - public hash: string; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public appId: App['id'] | null; - - @ManyToOne(type => App, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: App | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public description: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public iconUrl: string | null; - - @Column('varchar', { - length: 64, array: true, - default: '{}', - }) - public permission: string[]; - - @Column('boolean', { - default: false, - }) - public fetched: boolean; -} diff --git a/packages/backend/src/models/entities/ad.ts b/packages/backend/src/models/entities/ad.ts deleted file mode 100644 index 36b758f205..0000000000 --- a/packages/backend/src/models/entities/ad.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class Ad { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Ad.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - comment: 'The expired date of the Ad.', - }) - public expiresAt: Date; - - @Column('varchar', { - length: 32, nullable: false, - }) - public place: string; - - // 今は使われていないが将来的に活用される可能性はある - @Column('varchar', { - length: 32, nullable: false, - }) - public priority: string; - - @Column('integer', { - default: 1, nullable: false, - }) - public ratio: number; - - @Column('varchar', { - length: 1024, nullable: false, - }) - public url: string; - - @Column('varchar', { - length: 1024, nullable: false, - }) - public imageUrl: string; - - @Column('varchar', { - length: 8192, nullable: false, - }) - public memo: string; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/announcement-read.ts b/packages/backend/src/models/entities/announcement-read.ts deleted file mode 100644 index e4d256a864..0000000000 --- a/packages/backend/src/models/entities/announcement-read.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Announcement } from './announcement.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'announcementId'], { unique: true }) -export class AnnouncementRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the AnnouncementRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public announcementId: Announcement['id']; - - @ManyToOne(type => Announcement, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public announcement: Announcement | null; -} diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts deleted file mode 100644 index beb2f82462..0000000000 --- a/packages/backend/src/models/entities/announcement.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class Announcement { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Announcement.', - }) - public createdAt: Date; - - @Column('timestamp with time zone', { - comment: 'The updated date of the Announcement.', - nullable: true, - }) - public updatedAt: Date | null; - - @Column('varchar', { - length: 8192, nullable: false, - }) - public text: string; - - @Column('varchar', { - length: 256, nullable: false, - }) - public title: string; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public imageUrl: string | null; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/antenna-note.ts b/packages/backend/src/models/entities/antenna-note.ts deleted file mode 100644 index fcca493fe0..0000000000 --- a/packages/backend/src/models/entities/antenna-note.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Note } from './note.js'; -import { Antenna } from './antenna.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['noteId', 'antennaId'], { unique: true }) -export class AntennaNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The antenna ID.', - }) - public antennaId: Antenna['id']; - - @ManyToOne(type => Antenna, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public antenna: Antenna | null; - - @Index() - @Column('boolean', { - default: false, - }) - public read: boolean; -} diff --git a/packages/backend/src/models/entities/antenna.ts b/packages/backend/src/models/entities/antenna.ts deleted file mode 100644 index 6c8bb13e50..0000000000 --- a/packages/backend/src/models/entities/antenna.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { UserList } from './user-list.js'; -import { UserGroupJoining } from './user-group-joining.js'; - -@Entity() -export class Antenna { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the Antenna.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Antenna.', - }) - public name: string; - - @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) - public src: 'home' | 'all' | 'users' | 'list' | 'group'; - - @Column({ - ...id(), - nullable: true, - }) - public userListId: UserList['id'] | null; - - @ManyToOne(type => UserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: UserList | null; - - @Column({ - ...id(), - nullable: true, - }) - public userGroupJoiningId: UserGroupJoining['id'] | null; - - @ManyToOne(type => UserGroupJoining, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupJoining: UserGroupJoining | null; - - @Column('varchar', { - length: 1024, array: true, - default: '{}', - }) - public users: string[]; - - @Column('jsonb', { - default: [], - }) - public keywords: string[][]; - - @Column('jsonb', { - default: [], - }) - public excludeKeywords: string[][]; - - @Column('boolean', { - default: false, - }) - public caseSensitive: boolean; - - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - - @Column('boolean') - public withFile: boolean; - - @Column('varchar', { - length: 2048, nullable: true, - }) - public expression: string | null; - - @Column('boolean') - public notify: boolean; -} diff --git a/packages/backend/src/models/entities/app.ts b/packages/backend/src/models/entities/app.ts deleted file mode 100644 index 46c11548a5..0000000000 --- a/packages/backend/src/models/entities/app.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class App { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the App.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - nullable: true, - }) - public user: User | null; - - @Index() - @Column('varchar', { - length: 64, - comment: 'The secret key of the App.', - }) - public secret: string; - - @Column('varchar', { - length: 128, - comment: 'The name of the App.', - }) - public name: string; - - @Column('varchar', { - length: 512, - comment: 'The description of the App.', - }) - public description: string; - - @Column('varchar', { - length: 64, array: true, - comment: 'The permission of the App.', - }) - public permission: string[]; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The callbackUrl of the App.', - }) - public callbackUrl: string | null; -} diff --git a/packages/backend/src/models/entities/attestation-challenge.ts b/packages/backend/src/models/entities/attestation-challenge.ts deleted file mode 100644 index c40df23293..0000000000 --- a/packages/backend/src/models/entities/attestation-challenge.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class AttestationChallenge { - @PrimaryColumn(id()) - public id: string; - - @Index() - @PrimaryColumn(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 64, - comment: 'Hex-encoded sha256 hash of the challenge.', - }) - public challenge: string; - - @Column('timestamp with time zone', { - comment: 'The date challenge was created for expiry purposes.', - }) - public createdAt: Date; - - @Column('boolean', { - comment: - 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.', - default: false, - }) - public registrationChallenge: boolean; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts deleted file mode 100644 index 295d1b486c..0000000000 --- a/packages/backend/src/models/entities/auth-session.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.js'; -import { App } from './app.js'; -import { id } from '../id.js'; - -@Entity() -export class AuthSession { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the AuthSession.', - }) - public createdAt: Date; - - @Index() - @Column('varchar', { - length: 128, - }) - public token: string; - - @Column({ - ...id(), - nullable: true, - }) - public userId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - nullable: true, - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public appId: App['id']; - - @ManyToOne(type => App, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: App | null; -} diff --git a/packages/backend/src/models/entities/blocking.ts b/packages/backend/src/models/entities/blocking.ts deleted file mode 100644 index 4ac73a00b5..0000000000 --- a/packages/backend/src/models/entities/blocking.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['blockerId', 'blockeeId'], { unique: true }) -export class Blocking { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Blocking.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The blockee user ID.', - }) - public blockeeId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public blockee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The blocker user ID.', - }) - public blockerId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public blocker: User | null; -} diff --git a/packages/backend/src/models/entities/channel-following.ts b/packages/backend/src/models/entities/channel-following.ts deleted file mode 100644 index 029dd6cf1a..0000000000 --- a/packages/backend/src/models/entities/channel-following.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { Channel } from './channel.js'; - -@Entity() -@Index(['followerId', 'followeeId'], { unique: true }) -export class ChannelFollowing { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the ChannelFollowing.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The followee channel ID.', - }) - public followeeId: Channel['id']; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: Channel | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; -} diff --git a/packages/backend/src/models/entities/channel-note-pining.ts b/packages/backend/src/models/entities/channel-note-pining.ts deleted file mode 100644 index 23be3b69d4..0000000000 --- a/packages/backend/src/models/entities/channel-note-pining.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { Note } from './note.js'; -import { Channel } from './channel.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['channelId', 'noteId'], { unique: true }) -export class ChannelNotePining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the ChannelNotePining.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public channelId: Channel['id']; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: Channel | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/channel.ts b/packages/backend/src/models/entities/channel.ts deleted file mode 100644 index abf6668bd2..0000000000 --- a/packages/backend/src/models/entities/channel.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { DriveFile } from './drive-file.js'; - -@Entity() -export class Channel { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Channel.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - nullable: true, - }) - public lastNotedAt: Date | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Channel.', - }) - public name: string; - - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description of the Channel.', - }) - public description: string | null; - - @Column({ - ...id(), - nullable: true, - comment: 'The ID of banner Channel.', - }) - public bannerId: DriveFile['id'] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public banner: DriveFile | null; - - @Index() - @Column('integer', { - default: 0, - comment: 'The count of notes.', - }) - public notesCount: number; - - @Index() - @Column('integer', { - default: 0, - comment: 'The count of users.', - }) - public usersCount: number; -} diff --git a/packages/backend/src/models/entities/clip-note.ts b/packages/backend/src/models/entities/clip-note.ts deleted file mode 100644 index 6f36885508..0000000000 --- a/packages/backend/src/models/entities/clip-note.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Note } from './note.js'; -import { Clip } from './clip.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['noteId', 'clipId'], { unique: true }) -export class ClipNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The clip ID.', - }) - public clipId: Clip['id']; - - @ManyToOne(type => Clip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: Clip | null; -} diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts deleted file mode 100644 index 1386684c32..0000000000 --- a/packages/backend/src/models/entities/clip.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class Clip { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the Clip.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Clip.', - }) - public name: string; - - @Column('boolean', { - default: false, - }) - public isPublic: boolean; - - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description of the Clip.', - }) - public description: string | null; -} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts deleted file mode 100644 index d410b1d429..0000000000 --- a/packages/backend/src/models/entities/drive-file.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { DriveFolder } from './drive-folder.js'; - -@Entity() -@Index(['userId', 'folderId', 'id']) -export class DriveFile { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the DriveFile.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: 'The host of owner. It will be null if the user in local.', - }) - public userHost: string | null; - - @Index() - @Column('varchar', { - length: 32, - comment: 'The MD5 hash of the DriveFile.', - }) - public md5: string; - - @Column('varchar', { - length: 256, - comment: 'The file name of the DriveFile.', - }) - public name: string; - - @Index() - @Column('varchar', { - length: 128, - comment: 'The content type (MIME) of the DriveFile.', - }) - public type: string; - - @Column('integer', { - comment: 'The file size (bytes) of the DriveFile.', - }) - public size: number; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The comment of the DriveFile.', - }) - public comment: string | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The BlurHash string.', - }) - public blurhash: string | null; - - @Column('jsonb', { - default: {}, - comment: 'The any properties of the DriveFile. For example, it includes image width/height.', - }) - public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; - - @Column('boolean') - public storedInternal: boolean; - - @Column('varchar', { - length: 512, - comment: 'The URL of the DriveFile.', - }) - public url: string; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URL of the thumbnail of the DriveFile.', - }) - public thumbnailUrl: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URL of the webpublic of the DriveFile.', - }) - public webpublicUrl: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public webpublicType: string | null; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, nullable: true, - }) - public accessKey: string | null; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, nullable: true, - }) - public thumbnailAccessKey: string | null; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, nullable: true, - }) - public webpublicAccessKey: string | null; - - @Index() - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.', - }) - public uri: string | null; - - @Column('varchar', { - length: 512, nullable: true, - }) - public src: string | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The parent folder ID. If null, it means the DriveFile is located in root.', - }) - public folderId: DriveFolder['id'] | null; - - @ManyToOne(type => DriveFolder, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public folder: DriveFolder | null; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is NSFW.', - }) - public isSensitive: boolean; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is NSFW. (predict)', - }) - public maybeSensitive: boolean; - - @Index() - @Column('boolean', { - default: false, - }) - public maybePorn: boolean; - - /** - * 外部の(信頼されていない)URLへの直リンクか否か - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is direct link to remote server.', - }) - public isLink: boolean; - - @Column('jsonb', { - default: {}, - nullable: true, - }) - public requestHeaders: Record | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public requestIp: string | null; -} diff --git a/packages/backend/src/models/entities/drive-folder.ts b/packages/backend/src/models/entities/drive-folder.ts deleted file mode 100644 index d4022c6ebc..0000000000 --- a/packages/backend/src/models/entities/drive-folder.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class DriveFolder { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the DriveFolder.', - }) - public createdAt: Date; - - @Column('varchar', { - length: 128, - comment: 'The name of the DriveFolder.', - }) - public name: string; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.', - }) - public parentId: DriveFolder['id'] | null; - - @ManyToOne(type => DriveFolder, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public parent: DriveFolder | null; -} diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts deleted file mode 100644 index 7332dd1857..0000000000 --- a/packages/backend/src/models/entities/emoji.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -@Index(['name', 'host'], { unique: true }) -export class Emoji { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - nullable: true, - }) - public updatedAt: Date | null; - - @Index() - @Column('varchar', { - length: 128, - }) - public name: string; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - }) - public host: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public category: string | null; - - @Column('varchar', { - length: 512, - }) - public originalUrl: string; - - @Column('varchar', { - length: 512, - default: '', - }) - public publicUrl: string; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - // publicUrlの方のtypeが入る - @Column('varchar', { - length: 64, nullable: true, - }) - public type: string | null; - - @Column('varchar', { - array: true, length: 128, default: '{}', - }) - public aliases: string[]; -} diff --git a/packages/backend/src/models/entities/follow-request.ts b/packages/backend/src/models/entities/follow-request.ts deleted file mode 100644 index 89946f6d35..0000000000 --- a/packages/backend/src/models/entities/follow-request.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['followerId', 'followeeId'], { unique: true }) -export class FollowRequest { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the FollowRequest.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The followee user ID.', - }) - public followeeId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'id of Follow Activity.', - }) - public requestId: string | null; - - //#region Denormalized fields - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followerHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerSharedInbox: string | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followeeHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeSharedInbox: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/following.ts b/packages/backend/src/models/entities/following.ts deleted file mode 100644 index b283ca7e8a..0000000000 --- a/packages/backend/src/models/entities/following.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['followerId', 'followeeId'], { unique: true }) -export class Following { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Following.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The followee user ID.', - }) - public followeeId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followerHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerSharedInbox: string | null; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followeeHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeSharedInbox: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/gallery-like.ts b/packages/backend/src/models/entities/gallery-like.ts deleted file mode 100644 index 4ce166d194..0000000000 --- a/packages/backend/src/models/entities/gallery-like.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { GalleryPost } from './gallery-post.js'; - -@Entity() -@Index(['userId', 'postId'], { unique: true }) -export class GalleryLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public postId: GalleryPost['id']; - - @ManyToOne(type => GalleryPost, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public post: GalleryPost | null; -} diff --git a/packages/backend/src/models/entities/gallery-post.ts b/packages/backend/src/models/entities/gallery-post.ts deleted file mode 100644 index 774cb946e9..0000000000 --- a/packages/backend/src/models/entities/gallery-post.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { DriveFile } from './drive-file.js'; - -@Entity() -export class GalleryPost { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the GalleryPost.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - comment: 'The updated date of the GalleryPost.', - }) - public updatedAt: Date; - - @Column('varchar', { - length: 256, - }) - public title: string; - - @Column('varchar', { - length: 2048, nullable: true, - }) - public description: string | null; - - @Index() - @Column({ - ...id(), - comment: 'The ID of author.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public fileIds: DriveFile['id'][]; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the post is sensitive.', - }) - public isSensitive: boolean; - - @Index() - @Column('integer', { - default: 0, - }) - public likedCount: number; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public tags: string[]; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/hashtag.ts b/packages/backend/src/models/entities/hashtag.ts deleted file mode 100644 index 6bd991f629..0000000000 --- a/packages/backend/src/models/entities/hashtag.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class Hashtag { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 128, - }) - public name: string; - - @Column({ - ...id(), - array: true, - }) - public mentionedUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public mentionedLocalUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedLocalUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public mentionedRemoteUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedRemoteUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedLocalUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedLocalUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedRemoteUserIds: User['id'][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedRemoteUsersCount: number; -} diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts deleted file mode 100644 index 7ea9234384..0000000000 --- a/packages/backend/src/models/entities/instance.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class Instance { - @PrimaryColumn(id()) - public id: string; - - /** - * このインスタンスを捕捉した日時 - */ - @Index() - @Column('timestamp with time zone', { - comment: 'The caught date of the Instance.', - }) - public caughtAt: Date; - - /** - * ホスト - */ - @Index({ unique: true }) - @Column('varchar', { - length: 128, - comment: 'The host of the Instance.', - }) - public host: string; - - /** - * インスタンスのユーザー数 - */ - @Column('integer', { - default: 0, - comment: 'The count of the users of the Instance.', - }) - public usersCount: number; - - /** - * インスタンスの投稿数 - */ - @Column('integer', { - default: 0, - comment: 'The count of the notes of the Instance.', - }) - public notesCount: number; - - /** - * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数 - */ - @Column('integer', { - default: 0, - }) - public followingCount: number; - - /** - * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数 - */ - @Column('integer', { - default: 0, - }) - public followersCount: number; - - /** - * 直近のリクエスト送信日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestRequestSentAt: Date | null; - - /** - * 直近のリクエスト送信時のHTTPステータスコード - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; - - /** - * 直近のリクエスト受信日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestRequestReceivedAt: Date | null; - - /** - * このインスタンスと最後にやり取りした日時 - */ - @Column('timestamp with time zone') - public lastCommunicatedAt: Date; - - /** - * このインスタンスと不通かどうか - */ - @Column('boolean', { - default: false, - }) - public isNotResponding: boolean; - - /** - * このインスタンスへの配信を停止するか - */ - @Index() - @Column('boolean', { - default: false, - }) - public isSuspended: boolean; - - @Column('varchar', { - length: 64, nullable: true, - comment: 'The software of the Instance.', - }) - public softwareName: string | null; - - @Column('varchar', { - length: 64, nullable: true, - }) - public softwareVersion: string | null; - - @Column('boolean', { - nullable: true, - }) - public openRegistrations: boolean | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public description: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerName: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public maintainerEmail: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public iconUrl: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public faviconUrl: string | null; - - @Column('varchar', { - length: 64, nullable: true, - }) - public themeColor: string | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public infoUpdatedAt: Date | null; -} diff --git a/packages/backend/src/models/entities/messaging-message.ts b/packages/backend/src/models/entities/messaging-message.ts deleted file mode 100644 index 099fb7aa01..0000000000 --- a/packages/backend/src/models/entities/messaging-message.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { DriveFile } from './drive-file.js'; -import { id } from '../id.js'; -import { UserGroup } from './user-group.js'; - -@Entity() -export class MessagingMessage { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the MessagingMessage.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The sender user ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient user ID.', - }) - public recipientId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public recipient: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient group ID.', - }) - public groupId: UserGroup['id'] | null; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public group: UserGroup | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public text: string | null; - - @Column('boolean', { - default: false, - }) - public isRead: boolean; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - @Column({ - ...id(), - array: true, default: '{}', - }) - public reads: User['id'][]; - - @Column({ - ...id(), - nullable: true, - }) - public fileId: DriveFile['id'] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public file: DriveFile | null; -} diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts deleted file mode 100644 index d33ff2519e..0000000000 --- a/packages/backend/src/models/entities/meta.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Clip } from './clip.js'; - -@Entity() -export class Meta { - @PrimaryColumn({ - type: 'varchar', - length: 32, - }) - public id: string; - - @Column('varchar', { - length: 128, nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public description: string | null; - - /** - * メンテナの名前 - */ - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerName: string | null; - - /** - * メンテナの連絡先 - */ - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerEmail: string | null; - - @Column('boolean', { - default: false, - }) - public disableRegistration: boolean; - - @Column('boolean', { - default: false, - }) - public disableLocalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public disableGlobalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public useStarForReactionFallback: boolean; - - @Column('varchar', { - length: 64, array: true, default: '{}', - }) - public langs: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public pinnedUsers: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public hiddenTags: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public blockedHosts: string[]; - - @Column('varchar', { - length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', - }) - public pinnedPages: string[]; - - @Column({ - ...id(), - nullable: true, - }) - public pinnedClipId: Clip['id'] | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public themeColor: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - default: '/assets/ai.png', - }) - public mascotImageUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public bannerUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public backgroundImageUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public logoImageUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - default: 'https://xn--931a.moe/aiart/yubitun.png', - }) - public errorImageUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public iconUrl: string | null; - - @Column('boolean', { - default: true, - }) - public cacheRemoteFiles: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public proxyAccountId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public proxyAccount: User | null; - - @Column('boolean', { - default: false, - }) - public emailRequiredForSignup: boolean; - - @Column('boolean', { - default: false, - }) - public enableHcaptcha: boolean; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public hcaptchaSiteKey: string | null; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public hcaptchaSecretKey: string | null; - - @Column('boolean', { - default: false, - }) - public enableRecaptcha: boolean; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public recaptchaSiteKey: string | null; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public recaptchaSecretKey: string | null; - - @Column('enum', { - enum: ['none', 'all', 'local', 'remote'], - default: 'none', - }) - public sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote'; - - @Column('enum', { - enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'], - default: 'medium', - }) - public sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh'; - - @Column('boolean', { - default: false, - }) - public setSensitiveFlagAutomatically: boolean; - - @Column('boolean', { - default: false, - }) - 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)', - }) - public remoteDriveCapacityMb: number; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public summalyProxy: string | null; - - @Column('boolean', { - default: false, - }) - public enableEmail: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public email: string | null; - - @Column('boolean', { - default: false, - }) - public smtpSecure: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public smtpHost: string | null; - - @Column('integer', { - nullable: true, - }) - public smtpPort: number | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public smtpUser: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public smtpPass: string | null; - - @Column('boolean', { - default: false, - }) - public enableServiceWorker: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public swPublicKey: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public swPrivateKey: string | null; - - @Column('boolean', { - default: false, - }) - public enableTwitterIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public twitterConsumerKey: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public twitterConsumerSecret: string | null; - - @Column('boolean', { - default: false, - }) - public enableGithubIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public githubClientId: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public githubClientSecret: string | null; - - @Column('boolean', { - default: false, - }) - public enableDiscordIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public discordClientId: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public discordClientSecret: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public deeplAuthKey: string | null; - - @Column('boolean', { - default: false, - }) - public deeplIsPro: boolean; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public ToSUrl: string | null; - - @Column('varchar', { - length: 512, - default: 'https://github.com/misskey-dev/misskey', - nullable: false, - }) - public repositoryUrl: string; - - @Column('varchar', { - length: 512, - default: 'https://github.com/misskey-dev/misskey/issues/new', - nullable: true, - }) - public feedbackUrl: string | null; - - @Column('varchar', { - length: 8192, - nullable: true, - }) - public defaultLightTheme: string | null; - - @Column('varchar', { - length: 8192, - nullable: true, - }) - public defaultDarkTheme: string | null; - - @Column('boolean', { - default: false, - }) - public useObjectStorage: boolean; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageBucket: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStoragePrefix: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageBaseUrl: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageEndpoint: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageRegion: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageAccessKey: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public objectStorageSecretKey: string | null; - - @Column('integer', { - nullable: true, - }) - public objectStoragePort: number | null; - - @Column('boolean', { - default: true, - }) - public objectStorageUseSSL: boolean; - - @Column('boolean', { - default: true, - }) - public objectStorageUseProxy: boolean; - - @Column('boolean', { - default: false, - }) - public objectStorageSetPublicRead: boolean; - - @Column('boolean', { - default: true, - }) - public objectStorageS3ForcePathStyle: boolean; - - @Column('boolean', { - default: false, - }) - public enableIpLogging: boolean; - - @Column('boolean', { - default: true, - }) - public enableActiveEmailValidation: boolean; -} diff --git a/packages/backend/src/models/entities/moderation-log.ts b/packages/backend/src/models/entities/moderation-log.ts deleted file mode 100644 index c99e550782..0000000000 --- a/packages/backend/src/models/entities/moderation-log.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class ModerationLog { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the ModerationLog.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - }) - public type: string; - - @Column('jsonb') - public info: Record; -} diff --git a/packages/backend/src/models/entities/muted-note.ts b/packages/backend/src/models/entities/muted-note.ts deleted file mode 100644 index 96a4fa8e33..0000000000 --- a/packages/backend/src/models/entities/muted-note.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { mutedNoteReasons } from '../../types.js'; - -@Entity() -@Index(['noteId', 'userId'], { unique: true }) -export class MutedNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - /** - * ミュートされた理由。 - */ - @Index() - @Column('enum', { - enum: mutedNoteReasons, - comment: 'The reason of the MutedNote.', - }) - public reason: typeof mutedNoteReasons[number]; -} diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts deleted file mode 100644 index 8f9e69063a..0000000000 --- a/packages/backend/src/models/entities/muting.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['muterId', 'muteeId'], { unique: true }) -export class Muting { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Muting.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - nullable: true, - }) - public expiresAt: Date | null; - - @Index() - @Column({ - ...id(), - comment: 'The mutee user ID.', - }) - public muteeId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public mutee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The muter user ID.', - }) - public muterId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public muter: User | null; -} diff --git a/packages/backend/src/models/entities/note-favorite.ts b/packages/backend/src/models/entities/note-favorite.ts deleted file mode 100644 index fe065b77a8..0000000000 --- a/packages/backend/src/models/entities/note-favorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the NoteFavorite.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/note-reaction.ts b/packages/backend/src/models/entities/note-reaction.ts deleted file mode 100644 index d7bc609898..0000000000 --- a/packages/backend/src/models/entities/note-reaction.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteReaction { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the NoteReaction.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user?: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note?: Note | null; - - // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) - - @Column('varchar', { - length: 260, - }) - public reaction: string; -} diff --git a/packages/backend/src/models/entities/note-thread-muting.ts b/packages/backend/src/models/entities/note-thread-muting.ts deleted file mode 100644 index 8c5f7bbab4..0000000000 --- a/packages/backend/src/models/entities/note-thread-muting.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'threadId'], { unique: true }) -export class NoteThreadMuting { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 256, - }) - public threadId: string; -} diff --git a/packages/backend/src/models/entities/note-unread.ts b/packages/backend/src/models/entities/note-unread.ts deleted file mode 100644 index a7acf254d3..0000000000 --- a/packages/backend/src/models/entities/note-unread.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { id } from '../id.js'; -import { Channel } from './channel.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteUnread { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - /** - * メンションか否か - */ - @Index() - @Column('boolean') - public isMentioned: boolean; - - /** - * ダイレクト投稿か否か - */ - @Index() - @Column('boolean') - public isSpecified: boolean; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public noteUserId: User['id']; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public noteChannelId: Channel['id'] | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/note-watching.ts b/packages/backend/src/models/entities/note-watching.ts deleted file mode 100644 index ed82e7dfe7..0000000000 --- a/packages/backend/src/models/entities/note-watching.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteWatching { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the NoteWatching.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The watcher ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The target Note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public noteUserId: Note['userId']; - //#endregion -} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts deleted file mode 100644 index 0ffeb85f69..0000000000 --- a/packages/backend/src/models/entities/note.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { DriveFile } from './drive-file.js'; -import { id } from '../id.js'; -import { noteVisibilities } from '../../types.js'; -import { Channel } from './channel.js'; - -@Entity() -@Index('IDX_NOTE_TAGS', { synchronize: false }) -@Index('IDX_NOTE_MENTIONS', { synchronize: false }) -@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) -export class Note { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Note.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The ID of reply target.', - }) - public replyId: Note['id'] | null; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public reply: Note | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The ID of renote target.', - }) - public renoteId: Note['id'] | null; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public renote: Note | null; - - @Index() - @Column('varchar', { - length: 256, nullable: true, - }) - public threadId: string | null; - - @Column('text', { - nullable: true, - }) - public text: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 512, nullable: true, - }) - public cw: string | null; - - @Index() - @Column({ - ...id(), - comment: 'The ID of author.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('boolean', { - default: false, - }) - public localOnly: boolean; - - @Column('smallint', { - default: 0, - }) - public renoteCount: number; - - @Column('smallint', { - default: 0, - }) - public repliesCount: number; - - @Column('jsonb', { - default: {}, - }) - public reactions: Record; - - /** - * public ... 公開 - * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す - * followers ... フォロワーのみ - * specified ... visibleUserIds で指定したユーザーのみ - */ - @Column('enum', { enum: noteVisibilities }) - public visibility: typeof noteVisibilities[number]; - - @Index({ unique: true }) - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URI of a note. it will be null when the note is local.', - }) - public uri: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The human readable url of a note. it will be null when the note is local.', - }) - public url: string | null; - - @Column('integer', { - default: 0, select: false, - }) - public score: number; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public fileIds: DriveFile['id'][]; - - @Index() - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public attachedFileTypes: string[]; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public visibleUserIds: User['id'][]; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public mentions: User['id'][]; - - @Column('text', { - default: '[]', - }) - public mentionedRemoteUsers: string; - - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public emojis: string[]; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public tags: string[]; - - @Column('boolean', { - default: false, - }) - public hasPoll: boolean; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The ID of source channel.', - }) - public channelId: Channel['id'] | null; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: Channel | null; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public userHost: string | null; - - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public replyUserId: User['id'] | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public replyUserHost: string | null; - - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public renoteUserId: User['id'] | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public renoteUserHost: string | null; - //#endregion - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} - -export type IMentionedRemoteUsers = { - uri: string; - url?: string; - username: string; - host: string; -}[]; diff --git a/packages/backend/src/models/entities/notification.ts b/packages/backend/src/models/entities/notification.ts deleted file mode 100644 index db3dba3632..0000000000 --- a/packages/backend/src/models/entities/notification.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { FollowRequest } from './follow-request.js'; -import { UserGroupInvitation } from './user-group-invitation.js'; -import { AccessToken } from './access-token.js'; -import { notificationTypes } from '@/types.js'; - -@Entity() -export class Notification { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Notification.', - }) - public createdAt: Date; - - /** - * 通知の受信者 - */ - @Index() - @Column({ - ...id(), - comment: 'The ID of recipient user of the Notification.', - }) - public notifieeId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public notifiee: User | null; - - /** - * 通知の送信者(initiator) - */ - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The ID of sender user of the Notification.', - }) - public notifierId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public notifier: User | null; - - /** - * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * renote - (自分または自分がWatchしている)投稿がRenoteされた - * quote - (自分または自分がWatchしている)投稿が引用Renoteされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * groupInvited - グループに招待された - * app - アプリ通知 - */ - @Index() - @Column('enum', { - enum: notificationTypes, - comment: 'The type of the Notification.', - }) - public type: typeof notificationTypes[number]; - - /** - * 通知が読まれたかどうか - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the Notification is read.', - }) - public isRead: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public noteId: Note['id'] | null; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column({ - ...id(), - nullable: true, - }) - public followRequestId: FollowRequest['id'] | null; - - @ManyToOne(type => FollowRequest, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followRequest: FollowRequest | null; - - @Column({ - ...id(), - nullable: true, - }) - public userGroupInvitationId: UserGroupInvitation['id'] | null; - - @ManyToOne(type => UserGroupInvitation, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupInvitation: UserGroupInvitation | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public reaction: string | null; - - @Column('integer', { - nullable: true, - }) - public choice: number | null; - - /** - * アプリ通知のbody - */ - @Column('varchar', { - length: 2048, nullable: true, - }) - public customBody: string | null; - - /** - * アプリ通知のheader - * (省略時はアプリ名で表示されることを期待) - */ - @Column('varchar', { - length: 256, nullable: true, - }) - public customHeader: string | null; - - /** - * アプリ通知のicon(URL) - * (省略時はアプリアイコンで表示されることを期待) - */ - @Column('varchar', { - length: 1024, nullable: true, - }) - public customIcon: string | null; - - /** - * アプリ通知のアプリ(のトークン) - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public appAccessTokenId: AccessToken['id'] | null; - - @ManyToOne(type => AccessToken, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public appAccessToken: AccessToken | null; -} diff --git a/packages/backend/src/models/entities/page-like.ts b/packages/backend/src/models/entities/page-like.ts deleted file mode 100644 index 17f4ebf520..0000000000 --- a/packages/backend/src/models/entities/page-like.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { Page } from './page.js'; - -@Entity() -@Index(['userId', 'pageId'], { unique: true }) -export class PageLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public pageId: Page['id']; - - @ManyToOne(type => Page, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public page: Page | null; -} diff --git a/packages/backend/src/models/entities/page.ts b/packages/backend/src/models/entities/page.ts deleted file mode 100644 index baad3a36fa..0000000000 --- a/packages/backend/src/models/entities/page.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; -import { DriveFile } from './drive-file.js'; - -@Entity() -@Index(['userId', 'name'], { unique: true }) -export class Page { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Page.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - comment: 'The updated date of the Page.', - }) - public updatedAt: Date; - - @Column('varchar', { - length: 256, - }) - public title: string; - - @Index() - @Column('varchar', { - length: 256, - }) - public name: string; - - @Column('varchar', { - length: 256, nullable: true, - }) - public summary: string | null; - - @Column('boolean') - public alignCenter: boolean; - - @Column('boolean', { - default: false, - }) - public hideTitleWhenPinned: boolean; - - @Column('varchar', { - length: 32, - }) - public font: string; - - @Index() - @Column({ - ...id(), - comment: 'The ID of author.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public eyeCatchingImageId: DriveFile['id'] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public eyeCatchingImage: DriveFile | null; - - @Column('jsonb', { - default: [], - }) - public content: Record[]; - - @Column('jsonb', { - default: [], - }) - public variables: Record[]; - - @Column('varchar', { - length: 16384, - default: '', - }) - public script: string; - - /** - * public ... 公開 - * followers ... フォロワーのみ - * specified ... visibleUserIds で指定したユーザーのみ - */ - @Column('enum', { enum: ['public', 'followers', 'specified'] }) - public visibility: 'public' | 'followers' | 'specified'; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public visibleUserIds: User['id'][]; - - @Column('integer', { - default: 0, - }) - public likedCount: number; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/password-reset-request.ts b/packages/backend/src/models/entities/password-reset-request.ts deleted file mode 100644 index 05e62cc5ab..0000000000 --- a/packages/backend/src/models/entities/password-reset-request.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; - -@Entity() -export class PasswordResetRequest { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public token: string; - - @Index() - @Column({ - ...id(), - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; -} diff --git a/packages/backend/src/models/entities/poll-vote.ts b/packages/backend/src/models/entities/poll-vote.ts deleted file mode 100644 index fca1cd0099..0000000000 --- a/packages/backend/src/models/entities/poll-vote.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId', 'choice'], { unique: true }) -export class PollVote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the PollVote.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('integer') - public choice: number; -} diff --git a/packages/backend/src/models/entities/poll.ts b/packages/backend/src/models/entities/poll.ts deleted file mode 100644 index 83d0873cc5..0000000000 --- a/packages/backend/src/models/entities/poll.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { noteVisibilities } from '../../types.js'; - -@Entity() -export class Poll { - @PrimaryColumn(id()) - public noteId: Note['id']; - - @OneToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public expiresAt: Date | null; - - @Column('boolean') - public multiple: boolean; - - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public choices: string[]; - - @Column('integer', { - array: true, - }) - public votes: number[]; - - //#region Denormalized fields - @Column('enum', { - enum: noteVisibilities, - comment: '[Denormalized]', - }) - public noteVisibility: typeof noteVisibilities[number]; - - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: User['id']; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public userHost: string | null; - //#endregion - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} - -export type IPoll = { - choices: string[]; - votes?: number[]; - multiple: boolean; - expiresAt: Date | null; -}; diff --git a/packages/backend/src/models/entities/promo-note.ts b/packages/backend/src/models/entities/promo-note.ts deleted file mode 100644 index d110b81e93..0000000000 --- a/packages/backend/src/models/entities/promo-note.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class PromoNote { - @PrimaryColumn(id()) - public noteId: Note['id']; - - @OneToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('timestamp with time zone') - public expiresAt: Date; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: User['id']; - //#endregion -} diff --git a/packages/backend/src/models/entities/promo-read.ts b/packages/backend/src/models/entities/promo-read.ts deleted file mode 100644 index a63b79cd1e..0000000000 --- a/packages/backend/src/models/entities/promo-read.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class PromoRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the PromoRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/registration-tickets.ts b/packages/backend/src/models/entities/registration-tickets.ts deleted file mode 100644 index 139e40f85e..0000000000 --- a/packages/backend/src/models/entities/registration-tickets.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class RegistrationTicket { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 64, - }) - public code: string; -} diff --git a/packages/backend/src/models/entities/registry-item.ts b/packages/backend/src/models/entities/registry-item.ts deleted file mode 100644 index 283796df91..0000000000 --- a/packages/backend/src/models/entities/registry-item.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい -@Entity() -export class RegistryItem { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the RegistryItem.', - }) - public createdAt: Date; - - @Column('timestamp with time zone', { - comment: 'The updated date of the RegistryItem.', - }) - public updatedAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 1024, - comment: 'The key of the RegistryItem.', - }) - public key: string; - - @Column('jsonb', { - default: {}, nullable: true, - comment: 'The value of the RegistryItem.', - }) - public value: any | null; - - @Index() - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public scope: string[]; - - // サードパーティアプリに開放するときのためのカラム - @Index() - @Column('varchar', { - length: 512, nullable: true, - }) - public domain: string | null; -} diff --git a/packages/backend/src/models/entities/relay.ts b/packages/backend/src/models/entities/relay.ts deleted file mode 100644 index 94d1929574..0000000000 --- a/packages/backend/src/models/entities/relay.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class Relay { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 512, nullable: false, - }) - public inbox: string; - - @Column('enum', { - enum: ['requesting', 'accepted', 'rejected'], - }) - public status: 'requesting' | 'accepted' | 'rejected'; -} diff --git a/packages/backend/src/models/entities/signin.ts b/packages/backend/src/models/entities/signin.ts deleted file mode 100644 index ba81f45e49..0000000000 --- a/packages/backend/src/models/entities/signin.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class Signin { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the Signin.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - }) - public ip: string; - - @Column('jsonb') - public headers: Record; - - @Column('boolean') - public success: boolean; -} diff --git a/packages/backend/src/models/entities/sw-subscription.ts b/packages/backend/src/models/entities/sw-subscription.ts deleted file mode 100644 index 59144d348b..0000000000 --- a/packages/backend/src/models/entities/sw-subscription.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class SwSubscription { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 512, - }) - public endpoint: string; - - @Column('varchar', { - length: 256, - }) - public auth: string; - - @Column('varchar', { - length: 128, - }) - public publickey: string; -} diff --git a/packages/backend/src/models/entities/used-username.ts b/packages/backend/src/models/entities/used-username.ts deleted file mode 100644 index eb90bef6ca..0000000000 --- a/packages/backend/src/models/entities/used-username.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PrimaryColumn, Entity, Column } from 'typeorm'; - -@Entity() -export class UsedUsername { - @PrimaryColumn('varchar', { - length: 128, - }) - public username: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user-group-invitation.ts b/packages/backend/src/models/entities/user-group-invitation.ts deleted file mode 100644 index 10f357049f..0000000000 --- a/packages/backend/src/models/entities/user-group-invitation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { UserGroup } from './user-group.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupInvitation { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupInvitation.', - }) - 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 group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/user-group-joining.ts b/packages/backend/src/models/entities/user-group-joining.ts deleted file mode 100644 index 62a814218a..0000000000 --- a/packages/backend/src/models/entities/user-group-joining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { UserGroup } from './user-group.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupJoining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupJoining.', - }) - 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 group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/user-group.ts b/packages/backend/src/models/entities/user-group.ts deleted file mode 100644 index 8d5de1d926..0000000000 --- a/packages/backend/src/models/entities/user-group.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class UserGroup { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroup.', - }) - public createdAt: Date; - - @Column('varchar', { - length: 256, - }) - public name: string; - - @Index() - @Column({ - ...id(), - comment: 'The ID of owner.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('boolean', { - default: false, - }) - public isPrivate: boolean; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user-ip.ts b/packages/backend/src/models/entities/user-ip.ts deleted file mode 100644 index 543e9e7289..0000000000 --- a/packages/backend/src/models/entities/user-ip.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.js'; - -@Entity() -@Index(['userId', 'ip'], { unique: true }) -export class UserIp { - @PrimaryGeneratedColumn() - public id: string; - - @Column('timestamp with time zone', { - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @Column('varchar', { - length: 128, - }) - public ip: string; -} diff --git a/packages/backend/src/models/entities/user-keypair.ts b/packages/backend/src/models/entities/user-keypair.ts deleted file mode 100644 index 85fa062977..0000000000 --- a/packages/backend/src/models/entities/user-keypair.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class UserKeypair { - @PrimaryColumn(id()) - public userId: User['id']; - - @OneToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 4096, - }) - public publicKey: string; - - @Column('varchar', { - length: 4096, - }) - public privateKey: string; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user-list-joining.ts b/packages/backend/src/models/entities/user-list-joining.ts deleted file mode 100644 index 12f28c4149..0000000000 --- a/packages/backend/src/models/entities/user-list-joining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { UserList } from './user-list.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'userListId'], { unique: true }) -export class UserListJoining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserListJoining.', - }) - 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 list ID.', - }) - public userListId: UserList['id']; - - @ManyToOne(type => UserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: UserList | null; -} diff --git a/packages/backend/src/models/entities/user-list.ts b/packages/backend/src/models/entities/user-list.ts deleted file mode 100644 index ca69394e93..0000000000 --- a/packages/backend/src/models/entities/user-list.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class UserList { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserList.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the UserList.', - }) - public name: string; -} diff --git a/packages/backend/src/models/entities/user-note-pining.ts b/packages/backend/src/models/entities/user-note-pining.ts deleted file mode 100644 index c91ab7fdd8..0000000000 --- a/packages/backend/src/models/entities/user-note-pining.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { Note } from './note.js'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class UserNotePining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserNotePinings.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/user-pending.ts b/packages/backend/src/models/entities/user-pending.ts deleted file mode 100644 index 7637948841..0000000000 --- a/packages/backend/src/models/entities/user-pending.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class UserPending { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 128, - }) - public code: string; - - @Column('varchar', { - length: 128, - }) - public username: string; - - @Column('varchar', { - length: 128, - }) - public email: string; - - @Column('varchar', { - length: 128, - }) - public password: string; -} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts deleted file mode 100644 index 3654b0a994..0000000000 --- a/packages/backend/src/models/entities/user-profile.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { ffVisibility, notificationTypes } from '@/types.js'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Page } from './page.js'; - -// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも -// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン -@Entity() -export class UserProfile { - @PrimaryColumn(id()) - public userId: User['id']; - - @OneToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The location of the User.', - }) - public location: string | null; - - @Column('char', { - length: 10, nullable: true, - comment: 'The birthday (YYYY-MM-DD) of the User.', - }) - public birthday: string | null; - - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description (bio) of the User.', - }) - public description: string | null; - - @Column('jsonb', { - default: [], - }) - public fields: { - name: string; - value: string; - }[]; - - @Column('varchar', { - length: 32, nullable: true, - }) - public lang: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'Remote URL of the user.', - }) - public url: string | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The email address of the User.', - }) - public email: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public emailVerifyCode: string | null; - - @Column('boolean', { - default: false, - }) - public emailVerified: boolean; - - @Column('jsonb', { - default: ['follow', 'receiveFollowRequest', 'groupInvited'], - }) - public emailNotificationTypes: string[]; - - @Column('boolean', { - default: false, - }) - public publicReactions: boolean; - - @Column('enum', { - enum: ffVisibility, - default: 'public', - }) - public ffVisibility: typeof ffVisibility[number]; - - @Column('varchar', { - length: 128, nullable: true, - }) - public twoFactorTempSecret: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public twoFactorSecret: string | null; - - @Column('boolean', { - default: false, - }) - public twoFactorEnabled: boolean; - - @Column('boolean', { - default: false, - }) - public securityKeysAvailable: boolean; - - @Column('boolean', { - default: false, - }) - public usePasswordLessLogin: boolean; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The password hash of the User. It will be null if the origin of the user is local.', - }) - public password: string | null; - - @Column('varchar', { - length: 8192, default: '', - }) - public moderationNote: string | null; - - // TODO: そのうち消す - @Column('jsonb', { - default: {}, - comment: 'The client-specific data of the User.', - }) - public clientData: Record; - - // TODO: そのうち消す - @Column('jsonb', { - default: {}, - comment: 'The room data of the User.', - }) - public room: Record; - - @Column('boolean', { - default: false, - }) - public autoAcceptFollowed: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether reject index by crawler.', - }) - public noCrawle: boolean; - - @Column('boolean', { - default: false, - }) - public alwaysMarkNsfw: boolean; - - @Column('boolean', { - default: false, - }) - public autoSensitive: boolean; - - @Column('boolean', { - default: false, - }) - public carefulBot: boolean; - - @Column('boolean', { - default: true, - }) - public injectFeaturedNote: boolean; - - @Column('boolean', { - default: true, - }) - public receiveAnnouncementEmail: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public pinnedPageId: Page['id'] | null; - - @OneToOne(type => Page, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public pinnedPage: Page | null; - - @Column('jsonb', { - default: {}, - }) - public integrations: Record; - - @Index() - @Column('boolean', { - default: false, select: false, - }) - public enableWordMute: boolean; - - @Column('jsonb', { - default: [], - }) - public mutedWords: string[][]; - - @Column('jsonb', { - default: [], - comment: 'List of instances muted by the user.', - }) - public mutedInstances: string[]; - - @Column('enum', { - enum: notificationTypes, - array: true, - default: [], - }) - public mutingNotificationTypes: typeof notificationTypes[number][]; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public userHost: string | null; - //#endregion - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user-publickey.ts b/packages/backend/src/models/entities/user-publickey.ts deleted file mode 100644 index 31ed60de82..0000000000 --- a/packages/backend/src/models/entities/user-publickey.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class UserPublickey { - @PrimaryColumn(id()) - public userId: User['id']; - - @OneToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public keyId: string; - - @Column('varchar', { - length: 4096, - }) - public keyPem: string; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user-security-key.ts b/packages/backend/src/models/entities/user-security-key.ts deleted file mode 100644 index c4f2a852e2..0000000000 --- a/packages/backend/src/models/entities/user-security-key.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -@Entity() -export class UserSecurityKey { - @PrimaryColumn('varchar', { - comment: 'Variable-length id given to navigator.credentials.get()', - }) - public id: string; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - comment: - 'Variable-length public key used to verify attestations (hex-encoded).', - }) - public publicKey: string; - - @Column('timestamp with time zone', { - comment: - 'The date of the last time the UserSecurityKey was successfully validated.', - }) - public lastUsed: Date; - - @Column('varchar', { - comment: 'User-defined name for this key', - length: 30, - }) - public name: string; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts deleted file mode 100644 index bc9446be41..0000000000 --- a/packages/backend/src/models/entities/user.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { DriveFile } from './drive-file.js'; - -@Entity() -@Index(['usernameLower', 'host'], { unique: true }) -export class User { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the User.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - nullable: true, - comment: 'The updated date of the User.', - }) - public updatedAt: Date | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public lastFetchedAt: Date | null; - - @Index() - @Column('timestamp with time zone', { - nullable: true, - }) - public lastActiveDate: Date | null; - - @Column('boolean', { - default: false, - }) - public hideOnlineStatus: boolean; - - @Column('varchar', { - length: 128, - comment: 'The username of the User.', - }) - public username: string; - - @Index() - @Column('varchar', { - length: 128, select: false, - comment: 'The username (lowercased) of the User.', - }) - public usernameLower: string; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The name of the User.', - }) - public name: string | null; - - @Column('integer', { - default: 0, - comment: 'The count of followers.', - }) - public followersCount: number; - - @Column('integer', { - default: 0, - comment: 'The count of following.', - }) - public followingCount: number; - - @Column('integer', { - default: 0, - comment: 'The count of notes.', - }) - public notesCount: number; - - @Column({ - ...id(), - nullable: true, - comment: 'The ID of avatar DriveFile.', - }) - public avatarId: DriveFile['id'] | null; - - @OneToOne(type => DriveFile, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public avatar: DriveFile | null; - - @Column({ - ...id(), - nullable: true, - comment: 'The ID of banner DriveFile.', - }) - public bannerId: DriveFile['id'] | null; - - @OneToOne(type => DriveFile, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public banner: DriveFile | null; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public tags: string[]; - - @Column('boolean', { - default: false, - comment: 'Whether the User is suspended.', - }) - 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.', - }) - public isLocked: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether the User is a bot.', - }) - public isBot: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether the User is a cat.', - }) - public isCat: boolean; - - @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.', - }) - public isModerator: boolean; - - @Index() - @Column('boolean', { - default: true, - comment: 'Whether the User is explorable.', - }) - public isExplorable: boolean; - - // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ - @Column('boolean', { - default: false, - comment: 'Whether the User is deleted.', - }) - public isDeleted: boolean; - - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public emojis: string[]; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: 'The host of the User. It will be null if the origin of the user is local.', - }) - public host: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The inbox URL of the User. It will be null if the origin of the user is local.', - }) - public inbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.', - }) - public sharedInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The featured URL of the User. It will be null if the origin of the user is local.', - }) - public featured: string | null; - - @Index() - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URI of the User. It will be null if the origin of the user is local.', - }) - public uri: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.', - }) - public followersUri: string | null; - - @Column('boolean', { - default: false, - comment: 'Whether to show users replying to other users in the timeline.', - }) - public showTimelineReplies: boolean; - - @Index({ unique: true }) - @Column('char', { - length: 16, nullable: true, unique: true, - comment: 'The native access token of the User. It will be null if the origin of the user is local.', - }) - 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; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} - -export interface ILocalUser extends User { - host: null; -} - -export interface IRemoteUser extends User { - host: string; -} - -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; diff --git a/packages/backend/src/models/entities/webhook.ts b/packages/backend/src/models/entities/webhook.ts deleted file mode 100644 index 56b411f879..0000000000 --- a/packages/backend/src/models/entities/webhook.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user.js'; -import { id } from '../id.js'; - -export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; - -@Entity() -export class Webhook { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the Antenna.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Antenna.', - }) - public name: string; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public on: (typeof webhookEventTypes)[number][]; - - @Column('varchar', { - length: 1024, - }) - public url: string; - - @Column('varchar', { - length: 1024, - }) - public secret: string; - - @Index() - @Column('boolean', { - default: true, - }) - public active: boolean; - - /** - * 直近のリクエスト送信日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestSentAt: Date | null; - - /** - * 直近のリクエスト送信時のHTTPステータスコード - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; -} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 3f73269318..559ed8c45a 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -1,133 +1,194 @@ -import { } from 'typeorm'; -import { db } from '@/db/postgre.js'; +import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import { AccessToken } from '@/models/entities/AccessToken.js'; +import { Ad } from '@/models/entities/Ad.js'; +import { Announcement } from '@/models/entities/Announcement.js'; +import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; +import { Antenna } from '@/models/entities/Antenna.js'; +import { AntennaNote } from '@/models/entities/AntennaNote.js'; +import { App } from '@/models/entities/App.js'; +import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; +import { AuthSession } from '@/models/entities/AuthSession.js'; +import { Blocking } from '@/models/entities/Blocking.js'; +import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; +import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js'; +import { Clip } from '@/models/entities/Clip.js'; +import { ClipNote } from '@/models/entities/ClipNote.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; +import { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { Emoji } from '@/models/entities/Emoji.js'; +import { Following } from '@/models/entities/Following.js'; +import { FollowRequest } from '@/models/entities/FollowRequest.js'; +import { GalleryLike } from '@/models/entities/GalleryLike.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { Hashtag } from '@/models/entities/Hashtag.js'; +import { Instance } from '@/models/entities/Instance.js'; +import { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import { Meta } from '@/models/entities/Meta.js'; +import { ModerationLog } from '@/models/entities/ModerationLog.js'; +import { MutedNote } from '@/models/entities/MutedNote.js'; +import { Muting } from '@/models/entities/Muting.js'; +import { Note } from '@/models/entities/Note.js'; +import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; +import { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; +import { NoteUnread } from '@/models/entities/NoteUnread.js'; +import { Notification } from '@/models/entities/Notification.js'; +import { Page } from '@/models/entities/Page.js'; +import { PageLike } from '@/models/entities/PageLike.js'; +import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; +import { Poll } from '@/models/entities/Poll.js'; +import { PollVote } from '@/models/entities/PollVote.js'; +import { PromoNote } from '@/models/entities/PromoNote.js'; +import { PromoRead } from '@/models/entities/PromoRead.js'; +import { RegistrationTicket } from '@/models/entities/RegistrationTickets.js'; +import { RegistryItem } from '@/models/entities/RegistryItem.js'; +import { Relay } from '@/models/entities/Relay.js'; +import { Signin } from '@/models/entities/Signin.js'; +import { SwSubscription } from '@/models/entities/SwSubscription.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { User } from '@/models/entities/User.js'; +import { UserGroup } from '@/models/entities/UserGroup.js'; +import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; +import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; +import { UserIp } from '@/models/entities/UserIp.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UserList } from '@/models/entities/UserList.js'; +import { UserListJoining } from '@/models/entities/UserListJoining.js'; +import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { UserPending } from '@/models/entities/UserPending.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; +import { Webhook } from '@/models/entities/Webhook.js'; +import { Channel } from '@/models/entities/Channel.js'; +import type { Repository } from 'typeorm'; -import { Announcement } from './entities/announcement.js'; -import { AnnouncementRead } from './entities/announcement-read.js'; -import { Instance } from './entities/instance.js'; -import { Poll } from './entities/poll.js'; -import { PollVote } from './entities/poll-vote.js'; -import { Meta } from './entities/meta.js'; -import { SwSubscription } from './entities/sw-subscription.js'; -import { NoteWatching } from './entities/note-watching.js'; -import { NoteThreadMuting } from './entities/note-thread-muting.js'; -import { NoteUnread } from './entities/note-unread.js'; -import { RegistrationTicket } from './entities/registration-tickets.js'; -import { UserRepository } from './repositories/user.js'; -import { NoteRepository } from './repositories/note.js'; -import { DriveFileRepository } from './repositories/drive-file.js'; -import { DriveFolderRepository } from './repositories/drive-folder.js'; -import { AccessToken } from './entities/access-token.js'; -import { UserNotePining } from './entities/user-note-pining.js'; -import { SigninRepository } from './repositories/signin.js'; -import { MessagingMessageRepository } from './repositories/messaging-message.js'; -import { UserListRepository } from './repositories/user-list.js'; -import { UserListJoining } from './entities/user-list-joining.js'; -import { UserGroupRepository } from './repositories/user-group.js'; -import { UserGroupJoining } from './entities/user-group-joining.js'; -import { UserGroupInvitationRepository } from './repositories/user-group-invitation.js'; -import { FollowRequestRepository } from './repositories/follow-request.js'; -import { MutingRepository } from './repositories/muting.js'; -import { BlockingRepository } from './repositories/blocking.js'; -import { NoteReactionRepository } from './repositories/note-reaction.js'; -import { NotificationRepository } from './repositories/notification.js'; -import { NoteFavoriteRepository } from './repositories/note-favorite.js'; -import { UserPublickey } from './entities/user-publickey.js'; -import { UserKeypair } from './entities/user-keypair.js'; -import { AppRepository } from './repositories/app.js'; -import { FollowingRepository } from './repositories/following.js'; -import { AbuseUserReportRepository } from './repositories/abuse-user-report.js'; -import { AuthSessionRepository } from './repositories/auth-session.js'; -import { UserProfile } from './entities/user-profile.js'; -import { AttestationChallenge } from './entities/attestation-challenge.js'; -import { UserSecurityKey } from './entities/user-security-key.js'; -import { HashtagRepository } from './repositories/hashtag.js'; -import { PageRepository } from './repositories/page.js'; -import { PageLikeRepository } from './repositories/page-like.js'; -import { GalleryPostRepository } from './repositories/gallery-post.js'; -import { GalleryLikeRepository } from './repositories/gallery-like.js'; -import { ModerationLogRepository } from './repositories/moderation-logs.js'; -import { UsedUsername } from './entities/used-username.js'; -import { ClipRepository } from './repositories/clip.js'; -import { ClipNote } from './entities/clip-note.js'; -import { AntennaRepository } from './repositories/antenna.js'; -import { AntennaNote } from './entities/antenna-note.js'; -import { PromoNote } from './entities/promo-note.js'; -import { PromoRead } from './entities/promo-read.js'; -import { EmojiRepository } from './repositories/emoji.js'; -import { RelayRepository } from './repositories/relay.js'; -import { ChannelRepository } from './repositories/channel.js'; -import { MutedNote } from './entities/muted-note.js'; -import { ChannelFollowing } from './entities/channel-following.js'; -import { ChannelNotePining } from './entities/channel-note-pining.js'; -import { RegistryItem } from './entities/registry-item.js'; -import { Ad } from './entities/ad.js'; -import { PasswordResetRequest } from './entities/password-reset-request.js'; -import { UserPending } from './entities/user-pending.js'; -import { InstanceRepository } from './repositories/instance.js'; -import { Webhook } from './entities/webhook.js'; -import { UserIp } from './entities/user-ip.js'; +export { + AbuseUserReport, + AccessToken, + Ad, + Announcement, + AnnouncementRead, + Antenna, + AntennaNote, + App, + AttestationChallenge, + AuthSession, + Blocking, + ChannelFollowing, + ChannelNotePining, + Clip, + ClipNote, + DriveFile, + DriveFolder, + Emoji, + Following, + FollowRequest, + GalleryLike, + GalleryPost, + Hashtag, + Instance, + MessagingMessage, + Meta, + ModerationLog, + MutedNote, + Muting, + Note, + NoteFavorite, + NoteReaction, + NoteThreadMuting, + NoteUnread, + Notification, + Page, + PageLike, + PasswordResetRequest, + Poll, + PollVote, + PromoNote, + PromoRead, + RegistrationTicket, + RegistryItem, + Relay, + Signin, + SwSubscription, + UsedUsername, + User, + UserGroup, + UserGroupInvitation, + UserGroupJoining, + UserIp, + UserKeypair, + UserList, + UserListJoining, + UserNotePining, + UserPending, + UserProfile, + UserPublickey, + UserSecurityKey, + Webhook, + Channel, +}; -export const Announcements = db.getRepository(Announcement); -export const AnnouncementReads = db.getRepository(AnnouncementRead); -export const Apps = (AppRepository); -export const Notes = (NoteRepository); -export const NoteFavorites = (NoteFavoriteRepository); -export const NoteWatchings = db.getRepository(NoteWatching); -export const NoteThreadMutings = db.getRepository(NoteThreadMuting); -export const NoteReactions = (NoteReactionRepository); -export const NoteUnreads = db.getRepository(NoteUnread); -export const Polls = db.getRepository(Poll); -export const PollVotes = db.getRepository(PollVote); -export const Users = (UserRepository); -export const UserProfiles = db.getRepository(UserProfile); -export const UserKeypairs = db.getRepository(UserKeypair); -export const UserPendings = db.getRepository(UserPending); -export const AttestationChallenges = db.getRepository(AttestationChallenge); -export const UserSecurityKeys = db.getRepository(UserSecurityKey); -export const UserPublickeys = db.getRepository(UserPublickey); -export const UserLists = (UserListRepository); -export const UserListJoinings = db.getRepository(UserListJoining); -export const UserGroups = (UserGroupRepository); -export const UserGroupJoinings = db.getRepository(UserGroupJoining); -export const UserGroupInvitations = (UserGroupInvitationRepository); -export const UserNotePinings = db.getRepository(UserNotePining); -export const UserIps = db.getRepository(UserIp); -export const UsedUsernames = db.getRepository(UsedUsername); -export const Followings = (FollowingRepository); -export const FollowRequests = (FollowRequestRepository); -export const Instances = (InstanceRepository); -export const Emojis = (EmojiRepository); -export const DriveFiles = (DriveFileRepository); -export const DriveFolders = (DriveFolderRepository); -export const Notifications = (NotificationRepository); -export const Metas = db.getRepository(Meta); -export const Mutings = (MutingRepository); -export const Blockings = (BlockingRepository); -export const SwSubscriptions = db.getRepository(SwSubscription); -export const Hashtags = (HashtagRepository); -export const AbuseUserReports = (AbuseUserReportRepository); -export const RegistrationTickets = db.getRepository(RegistrationTicket); -export const AuthSessions = (AuthSessionRepository); -export const AccessTokens = db.getRepository(AccessToken); -export const Signins = (SigninRepository); -export const MessagingMessages = (MessagingMessageRepository); -export const Pages = (PageRepository); -export const PageLikes = (PageLikeRepository); -export const GalleryPosts = (GalleryPostRepository); -export const GalleryLikes = (GalleryLikeRepository); -export const ModerationLogs = (ModerationLogRepository); -export const Clips = (ClipRepository); -export const ClipNotes = db.getRepository(ClipNote); -export const Antennas = (AntennaRepository); -export const AntennaNotes = db.getRepository(AntennaNote); -export const PromoNotes = db.getRepository(PromoNote); -export const PromoReads = db.getRepository(PromoRead); -export const Relays = (RelayRepository); -export const MutedNotes = db.getRepository(MutedNote); -export const Channels = (ChannelRepository); -export const ChannelFollowings = db.getRepository(ChannelFollowing); -export const ChannelNotePinings = db.getRepository(ChannelNotePining); -export const RegistryItems = db.getRepository(RegistryItem); -export const Webhooks = db.getRepository(Webhook); -export const Ads = db.getRepository(Ad); -export const PasswordResetRequests = db.getRepository(PasswordResetRequest); +export type AbuseUserReportsRepository = Repository; +export type AccessTokensRepository = Repository; +export type AdsRepository = Repository; +export type AnnouncementsRepository = Repository; +export type AnnouncementReadsRepository = Repository; +export type AntennasRepository = Repository; +export type AntennaNotesRepository = Repository; +export type AppsRepository = Repository; +export type AttestationChallengesRepository = Repository; +export type AuthSessionsRepository = Repository; +export type BlockingsRepository = Repository; +export type ChannelFollowingsRepository = Repository; +export type ChannelNotePiningsRepository = Repository; +export type ClipsRepository = Repository; +export type ClipNotesRepository = Repository; +export type DriveFilesRepository = Repository; +export type DriveFoldersRepository = Repository; +export type EmojisRepository = Repository; +export type FollowingsRepository = Repository; +export type FollowRequestsRepository = Repository; +export type GalleryLikesRepository = Repository; +export type GalleryPostsRepository = Repository; +export type HashtagsRepository = Repository; +export type InstancesRepository = Repository; +export type MessagingMessagesRepository = Repository; +export type MetasRepository = Repository; +export type ModerationLogsRepository = Repository; +export type MutedNotesRepository = Repository; +export type MutingsRepository = Repository; +export type NotesRepository = Repository; +export type NoteFavoritesRepository = Repository; +export type NoteReactionsRepository = Repository; +export type NoteThreadMutingsRepository = Repository; +export type NoteUnreadsRepository = Repository; +export type NotificationsRepository = Repository; +export type PagesRepository = Repository; +export type PageLikesRepository = Repository; +export type PasswordResetRequestsRepository = Repository; +export type PollsRepository = Repository; +export type PollVotesRepository = Repository; +export type PromoNotesRepository = Repository; +export type PromoReadsRepository = Repository; +export type RegistrationTicketsRepository = Repository; +export type RegistryItemsRepository = Repository; +export type RelaysRepository = Repository; +export type SigninsRepository = Repository; +export type SwSubscriptionsRepository = Repository; +export type UsedUsernamesRepository = Repository; +export type UsersRepository = Repository; +export type UserGroupsRepository = Repository; +export type UserGroupInvitationsRepository = Repository; +export type UserGroupJoiningsRepository = Repository; +export type UserIpsRepository = Repository; +export type UserKeypairsRepository = Repository; +export type UserListsRepository = Repository; +export type UserListJoiningsRepository = Repository; +export type UserNotePiningsRepository = Repository; +export type UserPendingsRepository = Repository; +export type UserProfilesRepository = Repository; +export type UserPublickeysRepository = Repository; +export type UserSecurityKeysRepository = Repository; +export type WebhooksRepository = Repository; +export type ChannelsRepository = Repository; diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts deleted file mode 100644 index 36d7ab90c5..0000000000 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Users } from '../index.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; -import { awaitAll } from '@/prelude/await-all.js'; - -export const AbuseUserReportRepository = db.getRepository(AbuseUserReport).extend({ - async pack( - src: AbuseUserReport['id'] | AbuseUserReport, - ) { - const report = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: report.id, - createdAt: report.createdAt.toISOString(), - comment: report.comment, - resolved: report.resolved, - reporterId: report.reporterId, - targetUserId: report.targetUserId, - assigneeId: report.assigneeId, - reporter: Users.pack(report.reporter || report.reporterId, null, { - detail: true, - }), - targetUser: Users.pack(report.targetUser || report.targetUserId, null, { - detail: true, - }), - assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, { - detail: true, - }) : null, - forwarded: report.forwarded, - }); - }, - - packMany( - reports: any[], - ) { - return Promise.all(reports.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/antenna.ts b/packages/backend/src/models/repositories/antenna.ts deleted file mode 100644 index 70180e2dec..0000000000 --- a/packages/backend/src/models/repositories/antenna.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { Packed } from '@/misc/schema.js'; -import { AntennaNotes, UserGroupJoinings } from '../index.js'; - -export const AntennaRepository = db.getRepository(Antenna).extend({ - async pack( - src: Antenna['id'] | Antenna, - ): Promise> { - const antenna = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - const hasUnreadNote = (await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false })) != null; - const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOneBy({ id: antenna.userGroupJoiningId }) : null; - - return { - id: antenna.id, - createdAt: antenna.createdAt.toISOString(), - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, - users: antenna.users, - caseSensitive: antenna.caseSensitive, - notify: antenna.notify, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - hasUnreadNote, - }; - }, -}); diff --git a/packages/backend/src/models/repositories/app.ts b/packages/backend/src/models/repositories/app.ts deleted file mode 100644 index e08dd6f0e3..0000000000 --- a/packages/backend/src/models/repositories/app.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { App } from '@/models/entities/app.js'; -import { AccessTokens } from '../index.js'; -import { Packed } from '@/misc/schema.js'; -import { User } from '../entities/user.js'; - -export const AppRepository = db.getRepository(App).extend({ - async pack( - src: App['id'] | App, - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean, - includeSecret?: boolean, - includeProfileImageIds?: boolean - } - ): Promise> { - const opts = Object.assign({ - detail: false, - includeSecret: false, - includeProfileImageIds: false, - }, options); - - const app = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: app.id, - name: app.name, - callbackUrl: app.callbackUrl, - permission: app.permission, - ...(opts.includeSecret ? { secret: app.secret } : {}), - ...(me ? { - isAuthorized: await AccessTokens.countBy({ - appId: app.id, - userId: me.id, - }).then(count => count > 0), - } : {}), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/auth-session.ts b/packages/backend/src/models/repositories/auth-session.ts deleted file mode 100644 index 3f1f6f4897..0000000000 --- a/packages/backend/src/models/repositories/auth-session.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Apps } from '../index.js'; -import { AuthSession } from '@/models/entities/auth-session.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { User } from '@/models/entities/user.js'; - -export const AuthSessionRepository = db.getRepository(AuthSession).extend({ - async pack( - src: AuthSession['id'] | AuthSession, - me?: { id: User['id'] } | null | undefined - ) { - const session = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: session.id, - app: Apps.pack(session.appId, me), - token: session.token, - }); - }, -}); diff --git a/packages/backend/src/models/repositories/blocking.ts b/packages/backend/src/models/repositories/blocking.ts deleted file mode 100644 index 1d569fb875..0000000000 --- a/packages/backend/src/models/repositories/blocking.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Users } from '../index.js'; -import { Blocking } from '@/models/entities/blocking.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import { User } from '@/models/entities/user.js'; - -export const BlockingRepository = db.getRepository(Blocking).extend({ - async pack( - src: Blocking['id'] | Blocking, - me?: { id: User['id'] } | null | undefined - ): Promise> { - const blocking = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: blocking.id, - createdAt: blocking.createdAt.toISOString(), - blockeeId: blocking.blockeeId, - blockee: Users.pack(blocking.blockeeId, me, { - detail: true, - }), - }); - }, - - packMany( - blockings: any[], - me: { id: User['id'] } - ) { - return Promise.all(blockings.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts deleted file mode 100644 index 213ac3671a..0000000000 --- a/packages/backend/src/models/repositories/channel.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Channel } from '@/models/entities/channel.js'; -import { Packed } from '@/misc/schema.js'; -import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index.js'; -import { User } from '@/models/entities/user.js'; - -export const ChannelRepository = db.getRepository(Channel).extend({ - async pack( - src: Channel['id'] | Channel, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - const channel = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - const meId = me ? me.id : null; - - const banner = channel.bannerId ? await DriveFiles.findOneBy({ id: channel.bannerId }) : null; - - const hasUnreadNote = meId ? (await NoteUnreads.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined; - - const following = meId ? await ChannelFollowings.findOneBy({ - followerId: meId, - followeeId: channel.id, - }) : null; - - return { - id: channel.id, - createdAt: channel.createdAt.toISOString(), - lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null, - name: channel.name, - description: channel.description, - userId: channel.userId, - bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null, - usersCount: channel.usersCount, - notesCount: channel.notesCount, - - ...(me ? { - isFollowing: following != null, - hasUnreadNote, - } : {}), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/clip.ts b/packages/backend/src/models/repositories/clip.ts deleted file mode 100644 index b4a342905e..0000000000 --- a/packages/backend/src/models/repositories/clip.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Clip } from '@/models/entities/clip.js'; -import { Packed } from '@/misc/schema.js'; -import { Users } from '../index.js'; -import { awaitAll } from '@/prelude/await-all.js'; - -export const ClipRepository = db.getRepository(Clip).extend({ - async pack( - src: Clip['id'] | Clip, - ): Promise> { - const clip = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: clip.id, - createdAt: clip.createdAt.toISOString(), - userId: clip.userId, - user: Users.pack(clip.user || clip.userId), - name: clip.name, - description: clip.description, - isPublic: clip.isPublic, - }); - }, - - packMany( - clips: Clip[], - ) { - return Promise.all(clips.map(x => this.pack(x))); - }, -}); - diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts deleted file mode 100644 index 0d589d4f11..0000000000 --- a/packages/backend/src/models/repositories/drive-file.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { User } from '@/models/entities/user.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { awaitAll, Promiseable } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import config from '@/config/index.js'; -import { query, appendQuery } from '@/prelude/url.js'; -import { Meta } from '@/models/entities/meta.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, DriveFolders } from '../index.js'; - -type PackOptions = { - detail?: boolean, - self?: boolean, - withUser?: boolean, -}; - -export const DriveFileRepository = db.getRepository(DriveFile).extend({ - validateFileName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) && - (name.indexOf('\\') === -1) && - (name.indexOf('/') === -1) && - (name.indexOf('..') === -1) - ); - }, - - getPublicProperties(file: DriveFile): DriveFile['properties'] { - if (file.properties.orientation != null) { - // TODO - //const properties = structuredClone(file.properties); - const properties = JSON.parse(JSON.stringify(file.properties)); - if (file.properties.orientation >= 5) { - [properties.width, properties.height] = [properties.height, properties.width]; - } - properties.orientation = undefined; - return properties; - } - - return file.properties; - }, - - getPublicUrl(file: DriveFile, thumbnail = false): string | null { - // リモートかつメディアプロキシ - if (file.uri != null && file.userHost != null && config.mediaProxy != null) { - return appendQuery(config.mediaProxy, query({ - url: file.uri, - thumbnail: thumbnail ? '1' : undefined, - })); - } - - // リモートかつ期限切れはローカルプロキシを試みる - if (file.uri != null && file.isLink && config.proxyRemoteFiles) { - const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey; - - if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 - return `${config.url}/files/${key}`; - } - } - - const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type); - - return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url); - }, - - async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise { - const id = typeof user === 'object' ? user.id : user; - - const { sum } = await this - .createQueryBuilder('file') - .where('file.userId = :id', { id: id }) - .andWhere('file.isLink = FALSE') - .select('SUM(file.size)', 'sum') - .getRawOne(); - - return parseInt(sum, 10) || 0; - }, - - async calcDriveUsageOfHost(host: string): Promise { - const { sum } = await this - .createQueryBuilder('file') - .where('file.userHost = :host', { host: toPuny(host) }) - .andWhere('file.isLink = FALSE') - .select('SUM(file.size)', 'sum') - .getRawOne(); - - return parseInt(sum, 10) || 0; - }, - - async calcDriveUsageOfLocal(): Promise { - const { sum } = await this - .createQueryBuilder('file') - .where('file.userHost IS NULL') - .andWhere('file.isLink = FALSE') - .select('SUM(file.size)', 'sum') - .getRawOne(); - - return parseInt(sum, 10) || 0; - }, - - async calcDriveUsageOfRemote(): Promise { - const { sum } = await this - .createQueryBuilder('file') - .where('file.userHost IS NOT NULL') - .andWhere('file.isLink = FALSE') - .select('SUM(file.size)', 'sum') - .getRawOne(); - - return parseInt(sum, 10) || 0; - }, - - async pack( - src: DriveFile['id'] | DriveFile, - options?: PackOptions, - ): Promise> { - const opts = Object.assign({ - detail: false, - self: false, - }, options); - - const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll>({ - id: file.id, - createdAt: file.createdAt.toISOString(), - name: file.name, - type: file.type, - md5: file.md5, - size: file.size, - isSensitive: file.isSensitive, - blurhash: file.blurhash, - properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), - comment: file.comment, - folderId: file.folderId, - folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { - detail: true, - }) : null, - userId: opts.withUser ? file.userId : null, - user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, - }); - }, - - async packNullable( - src: DriveFile['id'] | DriveFile, - options?: PackOptions, - ): Promise | null> { - const opts = Object.assign({ - detail: false, - self: false, - }, options); - - const file = typeof src === 'object' ? src : await this.findOneBy({ id: src }); - if (file == null) return null; - - return await awaitAll>({ - id: file.id, - createdAt: file.createdAt.toISOString(), - name: file.name, - type: file.type, - md5: file.md5, - size: file.size, - isSensitive: file.isSensitive, - blurhash: file.blurhash, - properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), - comment: file.comment, - folderId: file.folderId, - folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { - detail: true, - }) : null, - userId: opts.withUser ? file.userId : null, - user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, - }); - }, - - async packMany( - files: (DriveFile['id'] | DriveFile)[], - options?: PackOptions, - ): Promise[]> { - const items = await Promise.all(files.map(f => this.packNullable(f, options))); - return items.filter((x): x is Packed<'DriveFile'> => x != null); - }, -}); diff --git a/packages/backend/src/models/repositories/drive-folder.ts b/packages/backend/src/models/repositories/drive-folder.ts deleted file mode 100644 index ab5f3dab63..0000000000 --- a/packages/backend/src/models/repositories/drive-folder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { DriveFolders, DriveFiles } from '../index.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; - -export const DriveFolderRepository = db.getRepository(DriveFolder).extend({ - async pack( - src: DriveFolder['id'] | DriveFolder, - options?: { - detail: boolean - } - ): Promise> { - const opts = Object.assign({ - detail: false, - }, options); - - const folder = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: folder.id, - createdAt: folder.createdAt.toISOString(), - name: folder.name, - parentId: folder.parentId, - - ...(opts.detail ? { - foldersCount: DriveFolders.countBy({ - parentId: folder.id, - }), - filesCount: DriveFiles.countBy({ - folderId: folder.id, - }), - - ...(folder.parentId ? { - parent: this.pack(folder.parentId, { - detail: true, - }), - } : {}), - } : {}), - }); - }, -}); diff --git a/packages/backend/src/models/repositories/emoji.ts b/packages/backend/src/models/repositories/emoji.ts deleted file mode 100644 index a0d390d793..0000000000 --- a/packages/backend/src/models/repositories/emoji.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { Packed } from '@/misc/schema.js'; - -export const EmojiRepository = db.getRepository(Emoji).extend({ - async pack( - src: Emoji['id'] | Emoji, - ): Promise> { - const emoji = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: emoji.id, - aliases: emoji.aliases, - name: emoji.name, - category: emoji.category, - host: emoji.host, - // || emoji.originalUrl してるのは後方互換性のため - url: emoji.publicUrl || emoji.originalUrl, - }; - }, - - packMany( - emojis: any[], - ) { - return Promise.all(emojis.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/follow-request.ts b/packages/backend/src/models/repositories/follow-request.ts deleted file mode 100644 index c4a7203aa1..0000000000 --- a/packages/backend/src/models/repositories/follow-request.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { FollowRequest } from '@/models/entities/follow-request.js'; -import { Users } from '../index.js'; -import { User } from '@/models/entities/user.js'; - -export const FollowRequestRepository = db.getRepository(FollowRequest).extend({ - async pack( - src: FollowRequest['id'] | FollowRequest, - me?: { id: User['id'] } | null | undefined - ) { - const request = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: request.id, - follower: await Users.pack(request.followerId, me), - followee: await Users.pack(request.followeeId, me), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/following.ts b/packages/backend/src/models/repositories/following.ts deleted file mode 100644 index 46109244fa..0000000000 --- a/packages/backend/src/models/repositories/following.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Users } from '../index.js'; -import { Following } from '@/models/entities/following.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import { User } from '@/models/entities/user.js'; - -type LocalFollowerFollowing = Following & { - followerHost: null; - followerInbox: null; - followerSharedInbox: null; -}; - -type RemoteFollowerFollowing = Following & { - followerHost: string; - followerInbox: string; - followerSharedInbox: string; -}; - -type LocalFolloweeFollowing = Following & { - followeeHost: null; - followeeInbox: null; - followeeSharedInbox: null; -}; - -type RemoteFolloweeFollowing = Following & { - followeeHost: string; - followeeInbox: string; - followeeSharedInbox: string; -}; - -export const FollowingRepository = db.getRepository(Following).extend({ - isLocalFollower(following: Following): following is LocalFollowerFollowing { - return following.followerHost == null; - }, - - isRemoteFollower(following: Following): following is RemoteFollowerFollowing { - return following.followerHost != null; - }, - - isLocalFollowee(following: Following): following is LocalFolloweeFollowing { - return following.followeeHost == null; - }, - - isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { - return following.followeeHost != null; - }, - - async pack( - src: Following['id'] | Following, - me?: { id: User['id'] } | null | undefined, - opts?: { - populateFollowee?: boolean; - populateFollower?: boolean; - } - ): Promise> { - const following = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - if (opts == null) opts = {}; - - return await awaitAll({ - id: following.id, - createdAt: following.createdAt.toISOString(), - followeeId: following.followeeId, - followerId: following.followerId, - followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, { - detail: true, - }) : undefined, - follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, { - detail: true, - }) : undefined, - }); - }, - - packMany( - followings: any[], - me?: { id: User['id'] } | null | undefined, - opts?: { - populateFollowee?: boolean; - populateFollower?: boolean; - } - ) { - return Promise.all(followings.map(x => this.pack(x, me, opts))); - }, -}); diff --git a/packages/backend/src/models/repositories/gallery-like.ts b/packages/backend/src/models/repositories/gallery-like.ts deleted file mode 100644 index 08ca4962b8..0000000000 --- a/packages/backend/src/models/repositories/gallery-like.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { GalleryLike } from '@/models/entities/gallery-like.js'; -import { GalleryPosts } from '../index.js'; - -export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({ - async pack( - src: GalleryLike['id'] | GalleryLike, - me?: any - ) { - const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: like.id, - post: await GalleryPosts.pack(like.post || like.postId, me), - }; - }, - - packMany( - likes: any[], - me: any - ) { - return Promise.all(likes.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts deleted file mode 100644 index bb8d40b75e..0000000000 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { Packed } from '@/misc/schema.js'; -import { Users, DriveFiles, GalleryLikes } from '../index.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { User } from '@/models/entities/user.js'; - -export const GalleryPostRepository = db.getRepository(GalleryPost).extend({ - async pack( - src: GalleryPost['id'] | GalleryPost, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - const meId = me ? me.id : null; - const post = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: post.id, - createdAt: post.createdAt.toISOString(), - updatedAt: post.updatedAt.toISOString(), - userId: post.userId, - user: Users.pack(post.user || post.userId, me), - title: post.title, - description: post.description, - fileIds: post.fileIds, - files: DriveFiles.packMany(post.fileIds), - tags: post.tags.length > 0 ? post.tags : undefined, - isSensitive: post.isSensitive, - likedCount: post.likedCount, - isLiked: meId ? await GalleryLikes.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, - }); - }, - - packMany( - posts: GalleryPost[], - me?: { id: User['id'] } | null | undefined, - ) { - return Promise.all(posts.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/hashtag.ts b/packages/backend/src/models/repositories/hashtag.ts deleted file mode 100644 index e6c0e36f00..0000000000 --- a/packages/backend/src/models/repositories/hashtag.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Hashtag } from '@/models/entities/hashtag.js'; -import { Packed } from '@/misc/schema.js'; - -export const HashtagRepository = db.getRepository(Hashtag).extend({ - async pack( - src: Hashtag, - ): Promise> { - return { - tag: src.name, - mentionedUsersCount: src.mentionedUsersCount, - mentionedLocalUsersCount: src.mentionedLocalUsersCount, - mentionedRemoteUsersCount: src.mentionedRemoteUsersCount, - attachedUsersCount: src.attachedUsersCount, - attachedLocalUsersCount: src.attachedLocalUsersCount, - attachedRemoteUsersCount: src.attachedRemoteUsersCount, - }; - }, - - packMany( - hashtags: Hashtag[], - ) { - return Promise.all(hashtags.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts deleted file mode 100644 index 5f0fd8d582..0000000000 --- a/packages/backend/src/models/repositories/instance.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Instance } from '@/models/entities/instance.js'; -import { Packed } from '@/misc/schema.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; - -export const InstanceRepository = db.getRepository(Instance).extend({ - async pack( - instance: Instance, - ): Promise> { - const meta = await fetchMeta(); - return { - id: instance.id, - caughtAt: instance.caughtAt.toISOString(), - host: instance.host, - usersCount: instance.usersCount, - notesCount: instance.notesCount, - followingCount: instance.followingCount, - followersCount: instance.followersCount, - latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null, - lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), - isNotResponding: instance.isNotResponding, - isSuspended: instance.isSuspended, - isBlocked: meta.blockedHosts.includes(instance.host), - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - openRegistrations: instance.openRegistrations, - name: instance.name, - description: instance.description, - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, - }; - }, - - packMany( - instances: Instance[], - ) { - return Promise.all(instances.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/messaging-message.ts b/packages/backend/src/models/repositories/messaging-message.ts deleted file mode 100644 index 6c51c93ff7..0000000000 --- a/packages/backend/src/models/repositories/messaging-message.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { Users, DriveFiles, UserGroups } from '../index.js'; -import { Packed } from '@/misc/schema.js'; -import { User } from '@/models/entities/user.js'; - -export const MessagingMessageRepository = db.getRepository(MessagingMessage).extend({ - async pack( - src: MessagingMessage['id'] | MessagingMessage, - me?: { id: User['id'] } | null | undefined, - options?: { - populateRecipient?: boolean, - populateGroup?: boolean, - } - ): Promise> { - const opts = options || { - populateRecipient: true, - populateGroup: true, - }; - - const message = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: message.id, - createdAt: message.createdAt.toISOString(), - text: message.text, - userId: message.userId, - user: await Users.pack(message.user || message.userId, me), - recipientId: message.recipientId, - recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined, - groupId: message.groupId, - group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined, - fileId: message.fileId, - file: message.fileId ? await DriveFiles.pack(message.fileId) : null, - isRead: message.isRead, - reads: message.reads, - }; - }, -}); diff --git a/packages/backend/src/models/repositories/moderation-logs.ts b/packages/backend/src/models/repositories/moderation-logs.ts deleted file mode 100644 index 1488b1eabe..0000000000 --- a/packages/backend/src/models/repositories/moderation-logs.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Users } from '../index.js'; -import { ModerationLog } from '@/models/entities/moderation-log.js'; -import { awaitAll } from '@/prelude/await-all.js'; - -export const ModerationLogRepository = db.getRepository(ModerationLog).extend({ - async pack( - src: ModerationLog['id'] | ModerationLog, - ) { - const log = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: log.id, - createdAt: log.createdAt.toISOString(), - type: log.type, - info: log.info, - userId: log.userId, - user: Users.pack(log.user || log.userId, null, { - detail: true, - }), - }); - }, - - packMany( - reports: any[], - ) { - return Promise.all(reports.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/muting.ts b/packages/backend/src/models/repositories/muting.ts deleted file mode 100644 index 7891b10fb0..0000000000 --- a/packages/backend/src/models/repositories/muting.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Users } from '../index.js'; -import { Muting } from '@/models/entities/muting.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import { User } from '@/models/entities/user.js'; - -export const MutingRepository = db.getRepository(Muting).extend({ - async pack( - src: Muting['id'] | Muting, - me?: { id: User['id'] } | null | undefined - ): Promise> { - const muting = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: muting.id, - createdAt: muting.createdAt.toISOString(), - expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, - muteeId: muting.muteeId, - mutee: Users.pack(muting.muteeId, me, { - detail: true, - }), - }); - }, - - packMany( - mutings: any[], - me: { id: User['id'] } - ) { - return Promise.all(mutings.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts deleted file mode 100644 index 9bd97f9880..0000000000 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; -import { Notes } from '../index.js'; -import { User } from '@/models/entities/user.js'; - -export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ - async pack( - src: NoteFavorite['id'] | NoteFavorite, - me?: { id: User['id'] } | null | undefined - ) { - const favorite = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: favorite.id, - createdAt: favorite.createdAt.toISOString(), - noteId: favorite.noteId, - note: await Notes.pack(favorite.note || favorite.noteId, me), - }; - }, - - packMany( - favorites: any[], - me: { id: User['id'] } - ) { - return Promise.all(favorites.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts deleted file mode 100644 index 4deae51c93..0000000000 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { Notes, Users } from '../index.js'; -import { Packed } from '@/misc/schema.js'; -import { convertLegacyReaction } from '@/misc/reaction-lib.js'; -import { User } from '@/models/entities/user.js'; - -export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ - async pack( - src: NoteReaction['id'] | NoteReaction, - me?: { id: User['id'] } | null | undefined, - options?: { - withNote: boolean; - }, - ): Promise> { - const opts = Object.assign({ - withNote: false, - }, options); - - const reaction = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: reaction.id, - createdAt: reaction.createdAt.toISOString(), - user: await Users.pack(reaction.user ?? reaction.userId, me), - type: convertLegacyReaction(reaction.reaction), - ...(opts.withNote ? { - note: await Notes.pack(reaction.note ?? reaction.noteId, me), - } : {}), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts deleted file mode 100644 index 3fefab0319..0000000000 --- a/packages/backend/src/models/repositories/note.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { In } from 'typeorm'; -import * as mfm from 'mfm-js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index.js'; -import { Packed } from '@/misc/schema.js'; -import { nyaize } from '@/misc/nyaize.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; -import { db } from '@/db/postgre.js'; - -async function hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { - // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) - let hide = false; - - // visibility が specified かつ自分が指定されていなかったら非表示 - if (packedNote.visibility === 'specified') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); - - if (specified) { - hide = false; - } else { - hide = true; - } - } - } - - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (packedNote.visibility === 'followers') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - const following = await Followings.findOneBy({ - followeeId: packedNote.userId, - followerId: meId, - }); - - if (following == null) { - hide = true; - } else { - hide = false; - } - } - } - - if (hide) { - packedNote.visibleUserIds = undefined; - packedNote.fileIds = []; - packedNote.files = []; - packedNote.text = null; - packedNote.poll = undefined; - packedNote.cw = null; - packedNote.isHidden = true; - } -} - -async function populatePoll(note: Note, meId: User['id'] | null) { - const poll = await Polls.findOneByOrFail({ noteId: note.id }); - const choices = poll.choices.map(c => ({ - text: c, - votes: poll.votes[poll.choices.indexOf(c)], - isVoted: false, - })); - - if (meId) { - if (poll.multiple) { - const votes = await PollVotes.findBy({ - userId: meId, - noteId: note.id, - }); - - const myChoices = votes.map(v => v.choice); - for (const myChoice of myChoices) { - choices[myChoice].isVoted = true; - } - } else { - const vote = await PollVotes.findOneBy({ - userId: meId, - noteId: note.id, - }); - - if (vote) { - choices[vote.choice].isVoted = true; - } - } - } - - return { - multiple: poll.multiple, - expiresAt: poll.expiresAt, - choices, - }; -} - -async function populateMyReaction(note: Note, meId: User['id'], _hint_?: { - myReactions: Map; -}) { - if (_hint_?.myReactions) { - const reaction = _hint_.myReactions.get(note.id); - if (reaction) { - return convertLegacyReaction(reaction.reaction); - } else if (reaction === null) { - return undefined; - } - // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない - } - - const reaction = await NoteReactions.findOneBy({ - userId: meId, - noteId: note.id, - }); - - if (reaction) { - return convertLegacyReaction(reaction.reaction); - } - - return undefined; -} - -export const NoteRepository = db.getRepository(Note).extend({ - async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { - // This code must always be synchronized with the checks in generateVisibilityQuery. - // visibility が specified かつ自分が指定されていなかったら非表示 - if (note.visibility === 'specified') { - if (meId == null) { - return false; - } else if (meId === note.userId) { - return true; - } else { - // 指定されているかどうか - return note.visibleUserIds.some((id: any) => meId === id); - } - } - - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (note.visibility === 'followers') { - if (meId == null) { - return false; - } else if (meId === note.userId) { - return true; - } else if (note.reply && (meId === note.reply.userId)) { - // 自分の投稿に対するリプライ - return true; - } else if (note.mentions && note.mentions.some(id => meId === id)) { - // 自分へのメンション - return true; - } else { - // フォロワーかどうか - const [following, user] = await Promise.all([ - Followings.count({ - where: { - followeeId: note.userId, - followerId: meId, - }, - take: 1, - }), - Users.findOneByOrFail({ id: meId }), - ]); - - /* If we know the following, everyhting is fine. - - But if we do not know the following, it might be that both the - author of the note and the author of the like are remote users, - in which case we can never know the following. Instead we have - to assume that the users are following each other. - */ - return following > 0 || (note.userHost != null && user.host != null); - } - } - - return true; - }, - - async pack( - src: Note['id'] | Note, - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - _hint_?: { - myReactions: Map; - }; - } - ): Promise> { - const opts = Object.assign({ - detail: true, - skipHide: false, - }, options); - - const meId = me ? me.id : null; - const note = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - const host = note.userHost; - - let text = note.text; - - if (note.name && (note.url ?? note.uri)) { - text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url ?? note.uri}`; - } - - const channel = note.channelId - ? note.channel - ? note.channel - : await Channels.findOneBy({ id: note.channelId }) - : null; - - const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, '')); - - const packed: Packed<'Note'> = await awaitAll({ - id: note.id, - createdAt: note.createdAt.toISOString(), - userId: note.userId, - user: Users.pack(note.user ?? note.userId, me, { - detail: false, - }), - text: text, - cw: note.cw, - visibility: note.visibility, - localOnly: note.localOnly || undefined, - visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, - renoteCount: note.renoteCount, - repliesCount: note.repliesCount, - reactions: convertLegacyReactions(note.reactions), - tags: note.tags.length > 0 ? note.tags : undefined, - emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host), - fileIds: note.fileIds, - files: DriveFiles.packMany(note.fileIds), - replyId: note.replyId, - renoteId: note.renoteId, - channelId: note.channelId || undefined, - channel: channel ? { - id: channel.id, - name: channel.name, - } : undefined, - mentions: note.mentions.length > 0 ? note.mentions : undefined, - uri: note.uri || undefined, - url: note.url || undefined, - - ...(opts.detail ? { - reply: note.replyId ? this.pack(note.reply || note.replyId, me, { - detail: false, - _hint_: options?._hint_, - }) : undefined, - - renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, { - detail: true, - _hint_: options?._hint_, - }) : undefined, - - poll: note.hasPoll ? populatePoll(note, meId) : undefined, - - ...(meId ? { - myReaction: populateMyReaction(note, meId, options?._hint_), - } : {}), - } : {}), - }); - - if (packed.user.isCat && packed.text) { - const tokens = packed.text ? mfm.parse(packed.text) : []; - mfm.inspect(tokens, node => { - if (node.type === 'text') { - // TODO: quoteなtextはskip - node.props.text = nyaize(node.props.text); - } - }); - packed.text = mfm.toString(tokens); - } - - if (!opts.skipHide) { - await hideNote(packed, meId); - } - - return packed; - }, - - async packMany( - notes: Note[], - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - } - ) { - if (notes.length === 0) return []; - - const meId = me ? me.id : null; - const myReactionsMap = new Map(); - if (meId) { - const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); - const targets = [...notes.map(n => n.id), ...renoteIds]; - const myReactions = await NoteReactions.findBy({ - userId: meId, - noteId: In(targets), - }); - - for (const target of targets) { - myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null); - } - } - - await prefetchEmojis(aggregateNoteEmojis(notes)); - - return await Promise.all(notes.map(n => this.pack(n, me, { - ...options, - _hint_: { - myReactions: myReactionsMap, - }, - }))); - }, -}); diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts deleted file mode 100644 index 42b47ab150..0000000000 --- a/packages/backend/src/models/repositories/notification.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { In, Repository } from 'typeorm'; -import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js'; -import { Notification } from '@/models/entities/notification.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { User } from '@/models/entities/user.js'; -import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; -import { notificationTypes } from '@/types.js'; -import { db } from '@/db/postgre.js'; - -export const NotificationRepository = db.getRepository(Notification).extend({ - async pack( - src: Notification['id'] | Notification, - options: { - _hintForEachNotes_?: { - myReactions: Map; - }; - } - ): Promise> { - const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null; - - return await awaitAll({ - id: notification.id, - createdAt: notification.createdAt.toISOString(), - type: notification.type, - isRead: notification.isRead, - userId: notification.notifierId, - user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null, - ...(notification.type === 'mention' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'reply' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'renote' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'quote' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'reaction' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - reaction: notification.reaction, - } : {}), - ...(notification.type === 'pollVote' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - choice: notification.choice, - } : {}), - ...(notification.type === 'pollEnded' ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'groupInvited' ? { - invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), - } : {}), - ...(notification.type === 'app' ? { - body: notification.customBody, - header: notification.customHeader || token?.name, - icon: notification.customIcon || token?.iconUrl, - } : {}), - }); - }, - - async packMany( - notifications: Notification[], - meId: User['id'] - ) { - if (notifications.length === 0) return []; - - const notes = notifications.filter(x => x.note != null).map(x => x.note!); - const noteIds = notes.map(n => n.id); - const myReactionsMap = new Map(); - const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); - const targets = [...noteIds, ...renoteIds]; - const myReactions = await NoteReactions.findBy({ - userId: meId, - noteId: In(targets), - }); - - for (const target of targets) { - myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null); - } - - await prefetchEmojis(aggregateNoteEmojis(notes)); - - return await Promise.all(notifications.map(x => this.pack(x, { - _hintForEachNotes_: { - myReactions: myReactionsMap, - }, - }))); - }, -}); diff --git a/packages/backend/src/models/repositories/page-like.ts b/packages/backend/src/models/repositories/page-like.ts deleted file mode 100644 index 87d6accc34..0000000000 --- a/packages/backend/src/models/repositories/page-like.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { PageLike } from '@/models/entities/page-like.js'; -import { Pages } from '../index.js'; -import { User } from '@/models/entities/user.js'; - -export const PageLikeRepository = db.getRepository(PageLike).extend({ - async pack( - src: PageLike['id'] | PageLike, - me?: { id: User['id'] } | null | undefined - ) { - const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: like.id, - page: await Pages.pack(like.page || like.pageId, me), - }; - }, - - packMany( - likes: any[], - me: { id: User['id'] } - ) { - return Promise.all(likes.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts deleted file mode 100644 index 092b26b396..0000000000 --- a/packages/backend/src/models/repositories/page.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Page } from '@/models/entities/page.js'; -import { Packed } from '@/misc/schema.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { User } from '@/models/entities/user.js'; -import { Users, DriveFiles, PageLikes } from '../index.js'; - -export const PageRepository = db.getRepository(Page).extend({ - async pack( - src: Page['id'] | Page, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - const meId = me ? me.id : null; - const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - const attachedFiles: Promise[] = []; - const collectFile = (xs: any[]) => { - for (const x of xs) { - if (x.type === 'image') { - attachedFiles.push(DriveFiles.findOneBy({ - id: x.fileId, - userId: page.userId, - })); - } - if (x.children) { - collectFile(x.children); - } - } - }; - collectFile(page.content); - - // 後方互換性のため - let migrated = false; - const migrate = (xs: any[]) => { - for (const x of xs) { - if (x.type === 'input') { - if (x.inputType === 'text') { - x.type = 'textInput'; - } - if (x.inputType === 'number') { - x.type = 'numberInput'; - if (x.default) x.default = parseInt(x.default, 10); - } - migrated = true; - } - if (x.children) { - migrate(x.children); - } - } - }; - migrate(page.content); - if (migrated) { - this.update(page.id, { - content: page.content, - }); - } - - return await awaitAll({ - id: page.id, - createdAt: page.createdAt.toISOString(), - updatedAt: page.updatedAt.toISOString(), - userId: page.userId, - user: Users.pack(page.user || page.userId, me), // { detail: true } すると無限ループするので注意 - content: page.content, - variables: page.variables, - title: page.title, - name: page.name, - summary: page.summary, - hideTitleWhenPinned: page.hideTitleWhenPinned, - alignCenter: page.alignCenter, - font: page.font, - script: page.script, - eyeCatchingImageId: page.eyeCatchingImageId, - eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, - attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), - likedCount: page.likedCount, - isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, - }); - }, - - packMany( - pages: Page[], - me?: { id: User['id'] } | null | undefined, - ) { - return Promise.all(pages.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/relay.ts b/packages/backend/src/models/repositories/relay.ts deleted file mode 100644 index fa1c8f4d8d..0000000000 --- a/packages/backend/src/models/repositories/relay.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Relay } from '@/models/entities/relay.js'; - -export const RelayRepository = db.getRepository(Relay).extend({ -}); diff --git a/packages/backend/src/models/repositories/signin.ts b/packages/backend/src/models/repositories/signin.ts deleted file mode 100644 index 94410ec58a..0000000000 --- a/packages/backend/src/models/repositories/signin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Signin } from '@/models/entities/signin.js'; - -export const SigninRepository = db.getRepository(Signin).extend({ - async pack( - src: Signin, - ) { - return src; - }, -}); diff --git a/packages/backend/src/models/repositories/user-group-invitation.ts b/packages/backend/src/models/repositories/user-group-invitation.ts deleted file mode 100644 index 79ad019c99..0000000000 --- a/packages/backend/src/models/repositories/user-group-invitation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; -import { UserGroups } from '../index.js'; - -export const UserGroupInvitationRepository = db.getRepository(UserGroupInvitation).extend({ - async pack( - src: UserGroupInvitation['id'] | UserGroupInvitation, - ) { - const invitation = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: invitation.id, - group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId), - }; - }, - - packMany( - invitations: any[], - ) { - return Promise.all(invitations.map(x => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/user-group.ts b/packages/backend/src/models/repositories/user-group.ts deleted file mode 100644 index 6eb9234244..0000000000 --- a/packages/backend/src/models/repositories/user-group.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { UserGroupJoinings } from '../index.js'; -import { Packed } from '@/misc/schema.js'; - -export const UserGroupRepository = db.getRepository(UserGroup).extend({ - async pack( - src: UserGroup['id'] | UserGroup, - ): Promise> { - const userGroup = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - const users = await UserGroupJoinings.findBy({ - userGroupId: userGroup.id, - }); - - return { - id: userGroup.id, - createdAt: userGroup.createdAt.toISOString(), - name: userGroup.name, - ownerId: userGroup.userId, - userIds: users.map(x => x.userId), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/user-list.ts b/packages/backend/src/models/repositories/user-list.ts deleted file mode 100644 index 2b6f411ef6..0000000000 --- a/packages/backend/src/models/repositories/user-list.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserListJoinings } from '../index.js'; -import { Packed } from '@/misc/schema.js'; - -export const UserListRepository = db.getRepository(UserList).extend({ - async pack( - src: UserList['id'] | UserList, - ): Promise> { - const userList = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - const users = await UserListJoinings.findBy({ - userListId: userList.id, - }); - - return { - id: userList.id, - createdAt: userList.createdAt.toISOString(), - name: userList.name, - userIds: users.map(x => x.userId), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts deleted file mode 100644 index 5c46ae27a3..0000000000 --- a/packages/backend/src/models/repositories/user.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { EntityRepository, Repository, In, Not } from 'typeorm'; -import Ajv from 'ajv'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import config from '@/config/index.js'; -import { Packed } from '@/misc/schema.js'; -import { awaitAll, Promiseable } from '@/prelude/await-all.js'; -import { populateEmojis } from '@/misc/populate-emojis.js'; -import { getAntennas } from '@/misc/antenna-cache.js'; -import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; -import { db } from '@/db/postgre.js'; -import { Instance } from '../entities/instance.js'; -import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; - -const userInstanceCache = new Cache(1000 * 60 * 60 * 3); - -type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; -type IsMeAndIsUserDetailed = - Detailed extends true ? - ExpectsMe extends true ? Packed<'MeDetailed'> : - ExpectsMe extends false ? Packed<'UserDetailedNotMe'> : - Packed<'UserDetailed'> : - Packed<'UserLite'>; - -const ajv = new Ajv(); - -const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; -const passwordSchema = { type: 'string', minLength: 1 } as const; -const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; -const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const; -const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; -const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; - -function isLocalUser(user: User): user is ILocalUser; -function isLocalUser(user: T): user is T & { host: null; }; -function isLocalUser(user: User | { host: User['host'] }): boolean { - return user.host == null; -} - -function isRemoteUser(user: User): user is IRemoteUser; -function isRemoteUser(user: T): user is T & { host: string; }; -function isRemoteUser(user: User | { host: User['host'] }): boolean { - return !isLocalUser(user); -} - -export const UserRepository = db.getRepository(User).extend({ - localUsernameSchema, - passwordSchema, - nameSchema, - descriptionSchema, - locationSchema, - birthdaySchema, - - //#region Validators - validateLocalUsername: ajv.compile(localUsernameSchema), - validatePassword: ajv.compile(passwordSchema), - validateName: ajv.compile(nameSchema), - validateDescription: ajv.compile(descriptionSchema), - validateLocation: ajv.compile(locationSchema), - validateBirthday: ajv.compile(birthdaySchema), - //#endregion - - async getRelation(me: User['id'], target: User['id']) { - return awaitAll({ - id: target, - isFollowing: Followings.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then(n => n > 0), - isFollowed: Followings.count({ - where: { - followerId: target, - followeeId: me, - }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestFromYou: FollowRequests.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestToYou: FollowRequests.count({ - where: { - followerId: target, - followeeId: me, - }, - take: 1, - }).then(n => n > 0), - isBlocking: Blockings.count({ - where: { - blockerId: me, - blockeeId: target, - }, - take: 1, - }).then(n => n > 0), - isBlocked: Blockings.count({ - where: { - blockerId: target, - blockeeId: me, - }, - take: 1, - }).then(n => n > 0), - isMuted: Mutings.count({ - where: { - muterId: me, - muteeId: target, - }, - take: 1, - }).then(n => n > 0), - }); - }, - - async getHasUnreadMessagingMessage(userId: User['id']): Promise { - const mute = await Mutings.findBy({ - muterId: userId, - }); - - const joinings = await UserGroupJoinings.findBy({ userId: userId }); - - const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') - .where('message.groupId = :groupId', { groupId: j.userGroupId }) - .andWhere('message.userId != :userId', { userId: userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) - .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない - .getOne().then(x => x != null))); - - const [withUser, withGroups] = await Promise.all([ - MessagingMessages.count({ - where: { - recipientId: userId, - isRead: false, - ...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}), - }, - take: 1, - }).then(count => count > 0), - groupQs, - ]); - - return withUser || withGroups.some(x => x); - }, - - async getHasUnreadAnnouncement(userId: User['id']): Promise { - const reads = await AnnouncementReads.findBy({ - userId: userId, - }); - - const count = await Announcements.countBy(reads.length > 0 ? { - id: Not(In(reads.map(read => read.announcementId))), - } : {}); - - return count > 0; - }, - - async getHasUnreadAntenna(userId: User['id']): Promise { - const myAntennas = (await getAntennas()).filter(a => a.userId === userId); - - const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({ - antennaId: In(myAntennas.map(x => x.id)), - read: false, - }) : null; - - return unread != null; - }, - - async getHasUnreadChannel(userId: User['id']): Promise { - const channels = await ChannelFollowings.findBy({ followerId: userId }); - - const unread = channels.length > 0 ? await NoteUnreads.findOneBy({ - userId: userId, - noteChannelId: In(channels.map(x => x.followeeId)), - }) : null; - - return unread != null; - }, - - async getHasUnreadNotification(userId: User['id']): Promise { - const mute = await Mutings.findBy({ - muterId: userId, - }); - const mutedUserIds = mute.map(m => m.muteeId); - - const count = await Notifications.count({ - where: { - notifieeId: userId, - ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), - isRead: false, - }, - take: 1, - }); - - return count > 0; - }, - - async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { - const count = await FollowRequests.countBy({ - followeeId: userId, - }); - - return count > 0; - }, - - getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { - if (user.hideOnlineStatus) return 'unknown'; - if (user.lastActiveDate == null) return 'unknown'; - const elapsed = Date.now() - user.lastActiveDate.getTime(); - return ( - elapsed < USER_ONLINE_THRESHOLD ? 'online' : - elapsed < USER_ACTIVE_THRESHOLD ? 'active' : - 'offline' - ); - }, - - async getAvatarUrl(user: User): Promise { - if (user.avatar) { - return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); - } else if (user.avatarId) { - const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId }); - return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id); - } else { - return this.getIdenticonUrl(user.id); - } - }, - - getAvatarUrlSync(user: User): string { - if (user.avatar) { - return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); - } else { - return this.getIdenticonUrl(user.id); - } - }, - - getIdenticonUrl(userId: User['id']): string { - return `${config.url}/identicon/${userId}`; - }, - - async pack( - src: User['id'] | User, - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: D, - includeSecrets?: boolean, - }, - ): Promise> { - const opts = Object.assign({ - detail: false, - includeSecrets: false, - }, options); - - let user: User; - - if (typeof src === 'object') { - user = src; - if (src.avatar === undefined && src.avatarId) src.avatar = await DriveFiles.findOneBy({ id: src.avatarId }) ?? null; - if (src.banner === undefined && src.bannerId) src.banner = await DriveFiles.findOneBy({ id: src.bannerId }) ?? null; - } else { - user = await this.findOneOrFail({ - where: { id: src }, - relations: { - avatar: true, - banner: true, - }, - }); - } - - const meId = me ? me.id : null; - const isMe = meId === user.id; - - const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; - const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin') - .where('pin.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('pin.note', 'note') - .orderBy('pin.id', 'DESC') - .getMany() : []; - const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null; - - const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : - null; - - const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : - null; - - const falsy = opts.detail ? false : undefined; - - const packed = { - id: user.id, - name: user.name, - username: user.username, - host: user.host, - avatarUrl: this.getAvatarUrlSync(user), - avatarBlurhash: user.avatar?.blurhash || null, - avatarColor: null, // 後方互換性のため - isAdmin: user.isAdmin || falsy, - isModerator: user.isModerator || falsy, - isBot: user.isBot || falsy, - isCat: user.isCat || falsy, - instance: user.host ? userInstanceCache.fetch(user.host, - () => Instances.findOneBy({ host: user.host! }), - v => v != null, - ).then(instance => instance ? { - name: instance.name, - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - } : undefined) : undefined, - emojis: populateEmojis(user.emojis, user.host), - onlineStatus: this.getOnlineStatus(user), - driveCapacityOverrideMb: user.driveCapacityOverrideMb, - - ...(opts.detail ? { - url: profile!.url, - uri: user.uri, - createdAt: user.createdAt.toISOString(), - updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, - lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.banner ? DriveFiles.getPublicUrl(user.banner, false) : null, - bannerBlurhash: user.banner?.blurhash || null, - bannerColor: null, // 後方互換性のため - isLocked: user.isLocked, - isSilenced: user.isSilenced || falsy, - isSuspended: user.isSuspended || falsy, - description: profile!.description, - location: profile!.location, - birthday: profile!.birthday, - lang: profile!.lang, - fields: profile!.fields, - followersCount: followersCount || 0, - followingCount: followingCount || 0, - notesCount: user.notesCount, - pinnedNoteIds: pins.map(pin => pin.noteId), - pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, { - detail: true, - }), - pinnedPageId: profile!.pinnedPageId, - pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null, - publicReactions: profile!.publicReactions, - ffVisibility: profile!.ffVisibility, - twoFactorEnabled: profile!.twoFactorEnabled, - usePasswordLessLogin: profile!.usePasswordLessLogin, - securityKeys: profile!.twoFactorEnabled - ? UserSecurityKeys.countBy({ - userId: user.id, - }).then(result => result >= 1) - : false, - } : {}), - - ...(opts.detail && isMe ? { - avatarId: user.avatarId, - bannerId: user.bannerId, - injectFeaturedNote: profile!.injectFeaturedNote, - receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, - alwaysMarkNsfw: profile!.alwaysMarkNsfw, - autoSensitive: profile!.autoSensitive, - carefulBot: profile!.carefulBot, - autoAcceptFollowed: profile!.autoAcceptFollowed, - noCrawle: profile!.noCrawle, - isExplorable: user.isExplorable, - isDeleted: user.isDeleted, - hideOnlineStatus: user.hideOnlineStatus, - hasUnreadSpecifiedNotes: NoteUnreads.count({ - where: { userId: user.id, isSpecified: true }, - take: 1, - }).then(count => count > 0), - hasUnreadMentions: NoteUnreads.count({ - where: { userId: user.id, isMentioned: true }, - take: 1, - }).then(count => count > 0), - hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), - hasUnreadAntenna: this.getHasUnreadAntenna(user.id), - hasUnreadChannel: this.getHasUnreadChannel(user.id), - hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), - hasUnreadNotification: this.getHasUnreadNotification(user.id), - hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), - integrations: profile!.integrations, - mutedWords: profile!.mutedWords, - mutedInstances: profile!.mutedInstances, - mutingNotificationTypes: profile!.mutingNotificationTypes, - emailNotificationTypes: profile!.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies || falsy, - } : {}), - - ...(opts.includeSecrets ? { - email: profile!.email, - emailVerified: profile!.emailVerified, - securityKeysList: profile!.twoFactorEnabled - ? UserSecurityKeys.find({ - where: { - userId: user.id, - }, - select: { - id: true, - name: true, - lastUsed: true, - }, - }) - : [], - } : {}), - - ...(relation ? { - isFollowing: relation.isFollowing, - isFollowed: relation.isFollowed, - hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou, - hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou, - isBlocking: relation.isBlocking, - isBlocked: relation.isBlocked, - isMuted: relation.isMuted, - } : {}), - } as Promiseable> as Promiseable>; - - return await awaitAll(packed); - }, - - packMany( - users: (User['id'] | User)[], - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: D, - includeSecrets?: boolean, - }, - ): Promise[]> { - return Promise.all(users.map(u => this.pack(u, me, options))); - }, - - isLocalUser, - isRemoteUser, -}); diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts index 93327304f3..c57b3fec19 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/schema/federation-instance.ts @@ -1,5 +1,3 @@ -import config from '@/config/index.js'; - export const packedFederationInstanceSchema = { type: 'object', properties: { @@ -64,7 +62,6 @@ export const packedFederationInstanceSchema = { softwareVersion: { type: 'string', optional: false, nullable: true, - example: config.version, }, openRegistrations: { type: 'boolean', diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts new file mode 100644 index 0000000000..f7044b374e --- /dev/null +++ b/packages/backend/src/postgre.ts @@ -0,0 +1,215 @@ +// https://github.com/typeorm/typeorm/issues/2400 +import pg from 'pg'; +pg.types.setTypeParser(20, Number); + +import { DataSource } from 'typeorm'; +import * as highlight from 'cli-highlight'; +import { entities as charts } from '@/core/chart/entities.js'; + +import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import { AccessToken } from '@/models/entities/AccessToken.js'; +import { Ad } from '@/models/entities/Ad.js'; +import { Announcement } from '@/models/entities/Announcement.js'; +import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; +import { Antenna } from '@/models/entities/Antenna.js'; +import { AntennaNote } from '@/models/entities/AntennaNote.js'; +import { App } from '@/models/entities/App.js'; +import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; +import { AuthSession } from '@/models/entities/AuthSession.js'; +import { Blocking } from '@/models/entities/Blocking.js'; +import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; +import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js'; +import { Clip } from '@/models/entities/Clip.js'; +import { ClipNote } from '@/models/entities/ClipNote.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; +import { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { Emoji } from '@/models/entities/Emoji.js'; +import { Following } from '@/models/entities/Following.js'; +import { FollowRequest } from '@/models/entities/FollowRequest.js'; +import { GalleryLike } from '@/models/entities/GalleryLike.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { Hashtag } from '@/models/entities/Hashtag.js'; +import { Instance } from '@/models/entities/Instance.js'; +import { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import { Meta } from '@/models/entities/Meta.js'; +import { ModerationLog } from '@/models/entities/ModerationLog.js'; +import { MutedNote } from '@/models/entities/MutedNote.js'; +import { Muting } from '@/models/entities/Muting.js'; +import { Note } from '@/models/entities/Note.js'; +import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; +import { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; +import { NoteUnread } from '@/models/entities/NoteUnread.js'; +import { Notification } from '@/models/entities/Notification.js'; +import { Page } from '@/models/entities/Page.js'; +import { PageLike } from '@/models/entities/PageLike.js'; +import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; +import { Poll } from '@/models/entities/Poll.js'; +import { PollVote } from '@/models/entities/PollVote.js'; +import { PromoNote } from '@/models/entities/PromoNote.js'; +import { PromoRead } from '@/models/entities/PromoRead.js'; +import { RegistrationTicket } from '@/models/entities/RegistrationTickets.js'; +import { RegistryItem } from '@/models/entities/RegistryItem.js'; +import { Relay } from '@/models/entities/Relay.js'; +import { Signin } from '@/models/entities/Signin.js'; +import { SwSubscription } from '@/models/entities/SwSubscription.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { User } from '@/models/entities/User.js'; +import { UserGroup } from '@/models/entities/UserGroup.js'; +import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; +import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; +import { UserIp } from '@/models/entities/UserIp.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UserList } from '@/models/entities/UserList.js'; +import { UserListJoining } from '@/models/entities/UserListJoining.js'; +import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { UserPending } from '@/models/entities/UserPending.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; +import { Webhook } from '@/models/entities/Webhook.js'; +import { Channel } from '@/models/entities/Channel.js'; + +import { loadConfig } from '@/config.js'; +import Logger from '@/logger.js'; +import { envOption } from './env.js'; + +export const dbLogger = new Logger('db'); + +const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); + +class MyCustomLogger implements Logger { + private highlight(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); + } + + public logQuery(query: string, parameters?: any[]) { + sqlLogger.info(this.highlight(query).substring(0, 100)); + } + + public logQueryError(error: string, query: string, parameters?: any[]) { + sqlLogger.error(this.highlight(query)); + } + + public logQuerySlow(time: number, query: string, parameters?: any[]) { + sqlLogger.warn(this.highlight(query)); + } + + public logSchemaBuild(message: string) { + sqlLogger.info(message); + } + + public log(message: string) { + sqlLogger.info(message); + } + + public logMigration(message: string) { + sqlLogger.info(message); + } +} + +export const entities = [ + Announcement, + AnnouncementRead, + Meta, + Instance, + App, + AuthSession, + AccessToken, + User, + UserProfile, + UserKeypair, + UserPublickey, + UserList, + UserListJoining, + UserGroup, + UserGroupJoining, + UserGroupInvitation, + UserNotePining, + UserSecurityKey, + UsedUsername, + AttestationChallenge, + Following, + FollowRequest, + Muting, + Blocking, + Note, + NoteFavorite, + NoteReaction, + NoteThreadMuting, + NoteUnread, + Page, + PageLike, + GalleryPost, + GalleryLike, + DriveFile, + DriveFolder, + Poll, + PollVote, + Notification, + Emoji, + Hashtag, + SwSubscription, + AbuseUserReport, + RegistrationTicket, + MessagingMessage, + Signin, + ModerationLog, + Clip, + ClipNote, + Antenna, + AntennaNote, + PromoNote, + PromoRead, + Relay, + MutedNote, + Channel, + ChannelFollowing, + ChannelNotePining, + RegistryItem, + Ad, + PasswordResetRequest, + UserPending, + Webhook, + UserIp, + ...charts, +]; + +const log = process.env.NODE_ENV !== 'production'; + +const config = loadConfig(); + +export function createPostgreDataSource() { + return new DataSource({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + extra: { + statement_timeout: 1000 * 10, + ...config.db.extra, + }, + synchronize: process.env.NODE_ENV === 'test', + dropSchema: process.env.NODE_ENV === 'test', + cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...) + type: 'ioredis', + options: { + host: config.redis.host, + port: config.redis.port, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + keyPrefix: `${config.redis.prefix}:query:`, + db: config.redis.db ?? 0, + }, + } : false, + logging: log, + logger: log ? new MyCustomLogger() : undefined, + maxQueryExecutionTime: 300, + entities: entities, + migrations: ['../../migration/*.js'], + }); +} diff --git a/packages/backend/src/prelude/README.md b/packages/backend/src/prelude/README.md deleted file mode 100644 index bb728cfb1b..0000000000 --- a/packages/backend/src/prelude/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Prelude -このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。 -Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。 diff --git a/packages/backend/src/prelude/array.ts b/packages/backend/src/prelude/array.ts deleted file mode 100644 index 0b2830cb7b..0000000000 --- a/packages/backend/src/prelude/array.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { EndoRelation, Predicate } from './relation.js'; - -/** - * Count the number of elements that satisfy the predicate - */ - -export function countIf(f: Predicate, xs: T[]): number { - return xs.filter(f).length; -} - -/** - * Count the number of elements that is equal to the element - */ -export function count(a: T, xs: T[]): number { - return countIf(x => x === a, xs); -} - -/** - * Concatenate an array of arrays - */ -export function concat(xss: T[][]): T[] { - return ([] as T[]).concat(...xss); -} - -/** - * Intersperse the element between the elements of the array - * @param sep The element to be interspersed - */ -export function intersperse(sep: T, xs: T[]): T[] { - return concat(xs.map(x => [sep, x])).slice(1); -} - -/** - * Returns the array of elements that is not equal to the element - */ -export function erase(a: T, xs: T[]): T[] { - return xs.filter(x => x !== a); -} - -/** - * Finds the array of all elements in the first array not contained in the second array. - * The order of result values are determined by the first array. - */ -export function difference(xs: T[], ys: T[]): T[] { - return xs.filter(x => !ys.includes(x)); -} - -/** - * Remove all but the first element from every group of equivalent elements - */ -export function unique(xs: T[]): T[] { - return [...new Set(xs)]; -} - -export function sum(xs: number[]): number { - return xs.reduce((a, b) => a + b, 0); -} - -export function maximum(xs: number[]): number { - return Math.max(...xs); -} - -/** - * Splits an array based on the equivalence relation. - * The concatenation of the result is equal to the argument. - */ -export function groupBy(f: EndoRelation, xs: T[]): T[][] { - const groups = [] as T[][]; - for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); - } else { - groups.push([x]); - } - } - return groups; -} - -/** - * Splits an array based on the equivalence relation induced by the function. - * The concatenation of the result is equal to the argument. - */ -export function groupOn(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record, item: T) => { - const key = keySelector(item); - if (!Object.prototype.hasOwnProperty.call(obj, key)) { - obj[key] = []; - } - - obj[key].push(item); - - return obj; - }, {}); -} - -/** - * Compare two arrays by lexicographical order - */ -export function lessThan(xs: number[], ys: number[]): boolean { - for (let i = 0; i < Math.min(xs.length, ys.length); i++) { - if (xs[i] < ys[i]) return true; - if (xs[i] > ys[i]) return false; - } - return xs.length < ys.length; -} - -/** - * Returns the longest prefix of elements that satisfy the predicate - */ -export function takeWhile(f: Predicate, xs: T[]): T[] { - const ys = []; - for (const x of xs) { - if (f(x)) { - ys.push(x); - } else { - break; - } - } - return ys; -} - -export function cumulativeSum(xs: number[]): number[] { - const ys = Array.from(xs); // deep copy - for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; - return ys; -} - -export function toArray(x: T | T[] | undefined): T[] { - return Array.isArray(x) ? x : x != null ? [x] : []; -} - -export function toSingle(x: T | T[] | undefined): T | undefined { - return Array.isArray(x) ? x[0] : x; -} diff --git a/packages/backend/src/prelude/await-all.ts b/packages/backend/src/prelude/await-all.ts deleted file mode 100644 index b955c3a5d8..0000000000 --- a/packages/backend/src/prelude/await-all.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Promiseable = { - [K in keyof T]: Promise | T[K]; -}; - -export async function awaitAll(obj: Promiseable): Promise { - const target = {} as T; - const keys = Object.keys(obj) as unknown as (keyof T)[]; - const values = Object.values(obj) as any[]; - - const resolvedValues = await Promise.all(values.map(value => - (!value || !value.constructor || value.constructor.name !== 'Object') - ? value - : awaitAll(value) - )); - - for (let i = 0; i < keys.length; i++) { - target[keys[i]] = resolvedValues[i]; - } - - return target; -} diff --git a/packages/backend/src/prelude/math.ts b/packages/backend/src/prelude/math.ts deleted file mode 100644 index 07b94bec30..0000000000 --- a/packages/backend/src/prelude/math.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function gcd(a: number, b: number): number { - return b === 0 ? a : gcd(b, a % b); -} diff --git a/packages/backend/src/prelude/maybe.ts b/packages/backend/src/prelude/maybe.ts deleted file mode 100644 index df7c4ed52a..0000000000 --- a/packages/backend/src/prelude/maybe.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface IMaybe { - isJust(): this is IJust; -} - -export interface IJust extends IMaybe { - get(): T; -} - -export function just(value: T): IJust { - return { - isJust: () => true, - get: () => value, - }; -} - -export function nothing(): IMaybe { - return { - isJust: () => false, - }; -} diff --git a/packages/backend/src/prelude/relation.ts b/packages/backend/src/prelude/relation.ts deleted file mode 100644 index 1f4703f52f..0000000000 --- a/packages/backend/src/prelude/relation.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type Predicate = (a: T) => boolean; - -export type Relation = (a: T, b: U) => boolean; - -export type EndoRelation = Relation; diff --git a/packages/backend/src/prelude/string.ts b/packages/backend/src/prelude/string.ts deleted file mode 100644 index b907e0a2e1..0000000000 --- a/packages/backend/src/prelude/string.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function concat(xs: string[]): string { - return xs.join(''); -} - -export function capitalize(s: string): string { - return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1)); -} - -export function toUpperCase(s: string): string { - return s.toUpperCase(); -} - -export function toLowerCase(s: string): string { - return s.toLowerCase(); -} diff --git a/packages/backend/src/prelude/symbol.ts b/packages/backend/src/prelude/symbol.ts deleted file mode 100644 index 51e12f7450..0000000000 --- a/packages/backend/src/prelude/symbol.ts +++ /dev/null @@ -1 +0,0 @@ -export const fallback = Symbol('fallback'); diff --git a/packages/backend/src/prelude/time.ts b/packages/backend/src/prelude/time.ts deleted file mode 100644 index 34e8b6b17c..0000000000 --- a/packages/backend/src/prelude/time.ts +++ /dev/null @@ -1,39 +0,0 @@ -const dateTimeIntervals = { - 'day': 86400000, - 'hour': 3600000, - 'ms': 1, -}; - -export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw 'wrong number of arguments'; - - return new Date(d); -} - -export function isTimeSame(a: Date, b: Date): boolean { - return a.getTime() === b.getTime(); -} - -export function isTimeBefore(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) < 0; -} - -export function isTimeAfter(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) > 0; -} - -export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() + (value * dateTimeIntervals[span])); -} - -export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() - (value * dateTimeIntervals[span])); -} diff --git a/packages/backend/src/prelude/url.ts b/packages/backend/src/prelude/url.ts deleted file mode 100644 index a4f2f7f5a8..0000000000 --- a/packages/backend/src/prelude/url.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function query(obj: Record): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); - - return Object.entries(params) - .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} diff --git a/packages/backend/src/prelude/xml.ts b/packages/backend/src/prelude/xml.ts deleted file mode 100644 index b4469a1d8d..0000000000 --- a/packages/backend/src/prelude/xml.ts +++ /dev/null @@ -1,41 +0,0 @@ -const map: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', -}; - -const beginingOfCDATA = ''; - -export function escapeValue(x: string): string { - let insideOfCDATA = false; - let builder = ''; - for ( - let i = 0; - i < x.length; - ) { - if (insideOfCDATA) { - if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) { - insideOfCDATA = true; - i += beginingOfCDATA.length; - } else { - builder += x[i++]; - } - } else { - if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) { - insideOfCDATA = false; - i += endOfCDATA.length; - } else { - const b = x[i++]; - builder += map[b] || b; - } - } - } - return builder; -} - -export function escapeAttribute(x: string): string { - return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x); -} diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts new file mode 100644 index 0000000000..fcc9873a6f --- /dev/null +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { DbJobData } from '@/queue/types.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; +import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; +import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; +import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; +import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; +import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; +import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; +import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; +import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; +import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; +import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; +import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; +import type Bull from 'bull'; + +@Injectable() +export class DbQueueProcessorsService { + constructor( + @Inject(DI.config) + private config: Config, + + private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, + private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, + private exportNotesProcessorService: ExportNotesProcessorService, + private exportFollowingProcessorService: ExportFollowingProcessorService, + private exportMutingProcessorService: ExportMutingProcessorService, + private exportBlockingProcessorService: ExportBlockingProcessorService, + private exportUserListsProcessorService: ExportUserListsProcessorService, + private importFollowingProcessorService: ImportFollowingProcessorService, + private importMutingProcessorService: ImportMutingProcessorService, + private importBlockingProcessorService: ImportBlockingProcessorService, + private importUserListsProcessorService: ImportUserListsProcessorService, + private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, + private deleteAccountProcessorService: DeleteAccountProcessorService, + ) { + } + + public start(dbQueue: Bull.Queue) { + const jobs = { + deleteDriveFiles: (job, done) => this.deleteDriveFilesProcessorService.process(job, done), + exportCustomEmojis: (job, done) => this.exportCustomEmojisProcessorService.process(job, done), + exportNotes: (job, done) => this.exportNotesProcessorService.process(job, done), + exportFollowing: (job, done) => this.exportFollowingProcessorService.process(job, done), + exportMuting: (job, done) => this.exportMutingProcessorService.process(job, done), + exportBlocking: (job, done) => this.exportBlockingProcessorService.process(job, done), + exportUserLists: (job, done) => this.exportUserListsProcessorService.process(job, done), + importFollowing: (job, done) => this.importFollowingProcessorService.process(job, done), + importMuting: (job, done) => this.importMutingProcessorService.process(job, done), + importBlocking: (job, done) => this.importBlockingProcessorService.process(job, done), + importUserLists: (job, done) => this.importUserListsProcessorService.process(job, done), + importCustomEmojis: (job, done) => this.importCustomEmojisProcessorService.process(job, done), + deleteAccount: (job, done) => this.deleteAccountProcessorService.process(job, done), + } as Record>>; + + for (const [k, v] of Object.entries(jobs)) { + dbQueue.process(k, v); + } + } +} diff --git a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts new file mode 100644 index 0000000000..402c038be0 --- /dev/null +++ b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts @@ -0,0 +1,30 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { ObjectStorageJobData } from '@/queue/types.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; +import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; +import type Bull from 'bull'; + +@Injectable() +export class ObjectStorageQueueProcessorsService { + constructor( + @Inject(DI.config) + private config: Config, + + private deleteFileProcessorService: DeleteFileProcessorService, + private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, + ) { + } + + public start(q: Bull.Queue) { + const jobs = { + deleteFile: (job, done) => this.deleteFileProcessorService.process(job, done), + cleanRemoteFiles: (job, done) => this.cleanRemoteFilesProcessorService.process(job, done), + } as Record>>; + + for (const [k, v] of Object.entries(jobs)) { + q.process(k, 16, v); + } + } +} diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts new file mode 100644 index 0000000000..4cdd4edfbb --- /dev/null +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Logger from '@/logger.js'; + +@Injectable() +export class QueueLoggerService { + public logger: Logger; + + constructor( + ) { + this.logger = new Logger('queue', 'orange'); + } +} diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts new file mode 100644 index 0000000000..f13dd3ef19 --- /dev/null +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -0,0 +1,72 @@ +import { Module } from '@nestjs/common'; +import { CoreModule } from '@/core/CoreModule.js'; +import { QueueLoggerService } from './QueueLoggerService.js'; +import { QueueProcessorService } from './QueueProcessorService.js'; +import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; +import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js'; +import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; +import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { InboxProcessorService } from './processors/InboxProcessorService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; +import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js'; +import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; +import { CleanProcessorService } from './processors/CleanProcessorService.js'; +import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; +import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; +import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; +import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; +import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; +import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; +import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; +import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; +import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; +import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; +import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; +import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; +import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; +import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; +import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; +import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; + +@Module({ + imports: [ + CoreModule, + ], + providers: [ + QueueLoggerService, + TickChartsProcessorService, + ResyncChartsProcessorService, + CleanChartsProcessorService, + CheckExpiredMutingsProcessorService, + CleanProcessorService, + DeleteDriveFilesProcessorService, + ExportCustomEmojisProcessorService, + ExportNotesProcessorService, + ExportFollowingProcessorService, + ExportMutingProcessorService, + ExportBlockingProcessorService, + ExportUserListsProcessorService, + ImportFollowingProcessorService, + ImportMutingProcessorService, + ImportBlockingProcessorService, + ImportUserListsProcessorService, + ImportCustomEmojisProcessorService, + DeleteAccountProcessorService, + DeleteFileProcessorService, + CleanRemoteFilesProcessorService, + SystemQueueProcessorsService, + ObjectStorageQueueProcessorsService, + DbQueueProcessorsService, + WebhookDeliverProcessorService, + EndedPollNotificationProcessorService, + DeliverProcessorService, + InboxProcessorService, + QueueProcessorService, + ], + exports: [ + QueueProcessorService, + ], +}) +export class QueueProcessorModule {} diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts new file mode 100644 index 0000000000..27afce0824 --- /dev/null +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -0,0 +1,141 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import type Logger from '@/logger.js'; +import { QueueService } from '@/core/QueueService.js'; +import { getJobInfo } from './get-job-info.js'; +import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js'; +import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js'; +import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; +import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; +import { InboxProcessorService } from './processors/InboxProcessorService.js'; +import { QueueLoggerService } from './QueueLoggerService.js'; + +@Injectable() +export class QueueProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private queueLoggerService: QueueLoggerService, + private queueService: QueueService, + private systemQueueProcessorsService: SystemQueueProcessorsService, + private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService, + private dbQueueProcessorsService: DbQueueProcessorsService, + private webhookDeliverProcessorService: WebhookDeliverProcessorService, + private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private deliverProcessorService: DeliverProcessorService, + private inboxProcessorService: InboxProcessorService, + ) { + this.#logger = this.queueLoggerService.logger; + } + + public start() { + function renderError(e: Error): any { + return { + stack: e.stack, + message: e.message, + name: e.name, + }; + } + + const systemLogger = this.#logger.createSubLogger('system'); + const deliverLogger = this.#logger.createSubLogger('deliver'); + const webhookLogger = this.#logger.createSubLogger('webhook'); + const inboxLogger = this.#logger.createSubLogger('inbox'); + const dbLogger = this.#logger.createSubLogger('db'); + const objectStorageLogger = this.#logger.createSubLogger('objectStorage'); + + this.queueService.systemQueue + .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); + + this.queueService.deliverQueue + .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) + .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + + this.queueService.inboxQueue + .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) + .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); + + this.queueService.dbQueue + .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); + + this.queueService.objectStorageQueue + .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); + + this.queueService.webhookDeliverQueue + .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) + .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + + this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job, done) => this.deliverProcessorService.process(job)); + this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job, done) => this.inboxProcessorService.process(job)); + this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); + this.queueService.webhookDeliverQueue.process(64, (job, done) => this.webhookDeliverProcessorService.process(job)); + this.dbQueueProcessorsService.start(this.queueService.dbQueue); + this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue); + + this.queueService.systemQueue.add('tickCharts', { + }, { + repeat: { cron: '55 * * * *' }, + removeOnComplete: true, + }); + + this.queueService.systemQueue.add('resyncCharts', { + }, { + repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.queueService.systemQueue.add('cleanCharts', { + }, { + repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.queueService.systemQueue.add('clean', { + }, { + repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.queueService.systemQueue.add('checkExpiredMutings', { + }, { + repeat: { cron: '*/5 * * * *' }, + removeOnComplete: true, + }); + + this.systemQueueProcessorsService.start(this.queueService.systemQueue); + } +} diff --git a/packages/backend/src/queue/SystemQueueProcessorsService.ts b/packages/backend/src/queue/SystemQueueProcessorsService.ts new file mode 100644 index 0000000000..7c227296e7 --- /dev/null +++ b/packages/backend/src/queue/SystemQueueProcessorsService.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; +import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; +import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; +import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { CleanProcessorService } from './processors/CleanProcessorService.js'; +import type Bull from 'bull'; + +@Injectable() +export class SystemQueueProcessorsService { + constructor( + @Inject(DI.config) + private config: Config, + + private tickChartsProcessorService: TickChartsProcessorService, + private resyncChartsProcessorService: ResyncChartsProcessorService, + private cleanChartsProcessorService: CleanChartsProcessorService, + private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, + private cleanProcessorService: CleanProcessorService, + ) { + } + + public start(dbQueue: Bull.Queue>) { + const jobs = { + tickCharts: (job, done) => this.tickChartsProcessorService.process(job, done), + resyncCharts: (job, done) => this.resyncChartsProcessorService.process(job, done), + cleanCharts: (job, done) => this.cleanChartsProcessorService.process(job, done), + checkExpiredMutings: (job, done) => this.checkExpiredMutingsProcessorService.process(job, done), + clean: (job, done) => this.cleanProcessorService.process(job, done), + } as Record> | Bull.ProcessPromiseFunction>>; + + for (const [k, v] of Object.entries(jobs)) { + dbQueue.process(k, v); + } + } +} diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts deleted file mode 100644 index ebb3a77cab..0000000000 --- a/packages/backend/src/queue/index.ts +++ /dev/null @@ -1,342 +0,0 @@ -import httpSignature from '@peertube/http-signature'; -import { v4 as uuid } from 'uuid'; - -import config from '@/config/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { IActivity } from '@/remote/activitypub/type.js'; -import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; -import { envOption } from '../env.js'; - -import processDeliver from './processors/deliver.js'; -import processInbox from './processors/inbox.js'; -import processDb from './processors/db/index.js'; -import processObjectStorage from './processors/object-storage/index.js'; -import processSystemQueue from './processors/system/index.js'; -import processWebhookDeliver from './processors/webhook-deliver.js'; -import { endedPollNotification } from './processors/ended-poll-notification.js'; -import { queueLogger } from './logger.js'; -import { getJobInfo } from './get-job-info.js'; -import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; -import { ThinUser } from './types.js'; - -function renderError(e: Error): any { - return { - stack: e.stack, - message: e.message, - name: e.name, - }; -} - -const systemLogger = queueLogger.createSubLogger('system'); -const deliverLogger = queueLogger.createSubLogger('deliver'); -const webhookLogger = queueLogger.createSubLogger('webhook'); -const inboxLogger = queueLogger.createSubLogger('inbox'); -const dbLogger = queueLogger.createSubLogger('db'); -const objectStorageLogger = queueLogger.createSubLogger('objectStorage'); - -systemQueue - .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); - -deliverQueue - .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); - -inboxQueue - .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); - -dbQueue - .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); - -objectStorageQueue - .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); - -webhookDeliverQueue - .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); - -export function deliver(user: ThinUser, content: unknown, to: string | null) { - if (content == null) return null; - if (to == null) return null; - - const data = { - user: { - id: user.id, - }, - content, - to, - }; - - return deliverQueue.add(data, { - attempts: config.deliverJobMaxAttempts || 12, - timeout: 1 * 60 * 1000, // 1min - backoff: { - type: 'apBackoff', - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { - const data = { - activity: activity, - signature, - }; - - return inboxQueue.add(data, { - attempts: config.inboxJobMaxAttempts || 8, - timeout: 5 * 60 * 1000, // 5min - backoff: { - type: 'apBackoff', - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteDriveFilesJob(user: ThinUser) { - return dbQueue.add('deleteDriveFiles', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportCustomEmojisJob(user: ThinUser) { - return dbQueue.add('exportCustomEmojis', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportNotesJob(user: ThinUser) { - return dbQueue.add('exportNotes', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { - return dbQueue.add('exportFollowing', { - user: user, - excludeMuting, - excludeInactive, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportMuteJob(user: ThinUser) { - return dbQueue.add('exportMute', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportBlockingJob(user: ThinUser) { - return dbQueue.add('exportBlocking', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportUserListsJob(user: ThinUser) { - return dbQueue.add('exportUserLists', { - user: user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importFollowing', { - user: user, - fileId: fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importMuting', { - user: user, - fileId: fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importBlocking', { - user: user, - fileId: fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importUserLists', { - user: user, - fileId: fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importCustomEmojis', { - user: user, - fileId: fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { - return dbQueue.add('deleteAccount', { - user: user, - soft: opts.soft, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteObjectStorageFileJob(key: string) { - return objectStorageQueue.add('deleteFile', { - key: key, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createCleanRemoteFilesJob() { - return objectStorageQueue.add('cleanRemoteFiles', {}, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { - const data = { - type, - content, - webhookId: webhook.id, - userId: webhook.userId, - to: webhook.url, - secret: webhook.secret, - createdAt: Date.now(), - eventId: uuid(), - }; - - return webhookDeliverQueue.add(data, { - attempts: 4, - timeout: 1 * 60 * 1000, // 1min - backoff: { - type: 'apBackoff', - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export default function() { - if (envOption.onlyServer) return; - - deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); - inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); - endedPollNotificationQueue.process(endedPollNotification); - webhookDeliverQueue.process(64, processWebhookDeliver); - processDb(dbQueue); - processObjectStorage(objectStorageQueue); - - systemQueue.add('tickCharts', { - }, { - repeat: { cron: '55 * * * *' }, - removeOnComplete: true, - }); - - systemQueue.add('resyncCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - systemQueue.add('cleanCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - systemQueue.add('clean', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - systemQueue.add('checkExpiredMutings', { - }, { - repeat: { cron: '*/5 * * * *' }, - removeOnComplete: true, - }); - - processSystemQueue(systemQueue); -} - -export function destroy() { - deliverQueue.once('cleaned', (jobs, status) => { - deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); - }); - deliverQueue.clean(0, 'delayed'); - - inboxQueue.once('cleaned', (jobs, status) => { - inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); - }); - inboxQueue.clean(0, 'delayed'); -} diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts deleted file mode 100644 index eef4080af3..0000000000 --- a/packages/backend/src/queue/initialize.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Bull from 'bull'; -import config from '@/config/index.js'; - -export function initialize(name: string, limitPerSec = -1) { - return new Bull(name, { - redis: { - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - db: config.redis.db || 0, - }, - prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', - limiter: limitPerSec > 0 ? { - max: limitPerSec, - duration: 1000, - } : undefined, - settings: { - backoffStrategies: { - apBackoff, - }, - }, - }); -} - -// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 -function apBackoff(attemptsMade: number, err: Error) { - const baseDelay = 60 * 1000; // 1min - const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; - backoff = Math.min(backoff, maxBackoff); - backoff += Math.round(backoff * Math.random() * 0.2); - return backoff; -} diff --git a/packages/backend/src/queue/logger.ts b/packages/backend/src/queue/logger.ts deleted file mode 100644 index 2843a3c263..0000000000 --- a/packages/backend/src/queue/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger.js'; - -export const queueLogger = new Logger('queue', 'orange'); diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts new file mode 100644 index 0000000000..514dc1dcf3 --- /dev/null +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { MutingsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; + +@Injectable() +export class CheckExpiredMutingsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private globalEventService: GlobalEventService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Checking expired mutings...'); + + const expired = await this.mutingsRepository.createQueryBuilder('muting') + .where('muting.expiresAt IS NOT NULL') + .andWhere('muting.expiresAt < :now', { now: new Date() }) + .innerJoinAndSelect('muting.mutee', 'mutee') + .getMany(); + + if (expired.length > 0) { + await this.mutingsRepository.delete({ + id: In(expired.map(m => m.id)), + }); + + for (const m of expired) { + this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!); + } + } + + this.#logger.succ('All expired mutings checked.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts new file mode 100644 index 0000000000..0eaad9b9ed --- /dev/null +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import DriveChart from '@/core/chart/charts/drive.js'; +import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; +import HashtagChart from '@/core/chart/charts/hashtag.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +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'; + +@Injectable() +export class CleanChartsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private federationChart: FederationChart, + private notesChart: NotesChart, + private usersChart: UsersChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + private perUserNotesChart: PerUserNotesChart, + private driveChart: DriveChart, + private perUserReactionsChart: PerUserReactionsChart, + private hashtagChart: HashtagChart, + private perUserFollowingChart: PerUserFollowingChart, + private perUserDriveChart: PerUserDriveChart, + private apRequestChart: ApRequestChart, + + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('clean-charts'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Clean charts...'); + + await Promise.all([ + this.federationChart.clean(), + this.notesChart.clean(), + this.usersChart.clean(), + this.activeUsersChart.clean(), + this.instanceChart.clean(), + this.perUserNotesChart.clean(), + this.driveChart.clean(), + this.perUserReactionsChart.clean(), + this.hashtagChart.clean(), + this.perUserFollowingChart.clean(), + this.perUserDriveChart.clean(), + this.apRequestChart.clean(), + ]); + + this.#logger.succ('All charts successfully cleaned.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts new file mode 100644 index 0000000000..6150120806 --- /dev/null +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -0,0 +1,36 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, LessThan, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UserIpsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; + +@Injectable() +export class CleanProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.userIpsRepository) + private userIpsRepository: UserIpsRepository, + + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('clean'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Cleaning...'); + + this.userIpsRepository.delete({ + createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), + }); + + this.#logger.succ('Cleaned.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts new file mode 100644 index 0000000000..8c53632563 --- /dev/null +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -0,0 +1,69 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; + +@Injectable() +export class CleanRemoteFilesProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('clean-remote-files'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Deleting cached remote files...'); + + let deletedCount = 0; + let cursor: any = null; + + while (true) { + const files = await this.driveFilesRepository.find({ + where: { + userHost: Not(IsNull()), + isLink: false, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 8, + order: { + id: 1, + }, + }); + + if (files.length === 0) { + job.progress(100); + break; + } + + cursor = files[files.length - 1].id; + + await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); + + deletedCount += 8; + + const total = await this.driveFilesRepository.countBy({ + userHost: Not(IsNull()), + isLink: false, + }); + + job.progress(deletedCount / total); + } + + this.#logger.succ('All cahced remote files has been deleted.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts new file mode 100644 index 0000000000..a5255c5c05 --- /dev/null +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -0,0 +1,124 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, UserProfilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; +import { EmailService } from '@/core/EmailService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserDeleteJobData } from '../types.js'; + +@Injectable() +export class DeleteAccountProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveService: DriveService, + private emailService: EmailService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('delete-account'); + } + + public async process(job: Bull.Job): Promise { + this.#logger.info(`Deleting account of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return; + } + + { // Delete notes + let cursor: Note['id'] | null = null; + + while (true) { + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Note[]; + + if (notes.length === 0) { + break; + } + + cursor = notes[notes.length - 1].id; + + await this.notesRepository.delete(notes.map(note => note.id)); + } + + this.#logger.succ('All of notes deleted'); + } + + { // Delete files + let cursor: DriveFile['id'] | null = null; + + while (true) { + const files = await this.driveFilesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 10, + order: { + id: 1, + }, + }) as DriveFile[]; + + if (files.length === 0) { + break; + } + + cursor = files[files.length - 1].id; + + for (const file of files) { + await this.driveService.deleteFileSync(file); + } + } + + this.#logger.succ('All of files deleted'); + } + + { // Send email notification + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + if (profile.email && profile.emailVerified) { + this.emailService.sendEmail(profile.email, 'Account deleted', + 'Your account has been deleted.', + 'Your account has been deleted.'); + } + } + + // soft指定されている場合は物理削除しない + if (job.data.soft) { + // nop + } else { + await this.usersRepository.delete(job.data.user.id); + } + + return 'Account deleted'; + } +} diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts new file mode 100644 index 0000000000..80814bb5a2 --- /dev/null +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -0,0 +1,78 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class DeleteDriveFilesProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('delete-drive-files'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Deleting drive files of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + let deletedCount = 0; + let cursor: any = null; + + while (true) { + const files = await this.driveFilesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (files.length === 0) { + job.progress(100); + break; + } + + cursor = files[files.length - 1].id; + + for (const file of files) { + await this.driveService.deleteFileSync(file); + deletedCount++; + } + + const total = await this.driveFilesRepository.countBy({ + userId: user.id, + }); + + job.progress(deletedCount / total); + } + + this.#logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); + done(); + } +} diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts new file mode 100644 index 0000000000..55424f6444 --- /dev/null +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -0,0 +1,31 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { ObjectStorageFileJobData } from '../types.js'; + +@Injectable() +export class DeleteFileProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('delete-file'); + } + + public async process(job: Bull.Job): Promise { + const key: string = job.data.key; + + await this.driveService.deleteObjectStorageFile(key); + + return 'Success'; + } +} diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts new file mode 100644 index 0000000000..3403ec83a9 --- /dev/null +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -0,0 +1,130 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApRequestService } from '@/core/remote/activitypub/ApRequestService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { Cache } from '@/misc/cache.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import ApRequestChart from '@/core/chart/charts/ap-request.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import { StatusError } from '@/misc/status-error.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DeliverJobData } from '../types.js'; + +@Injectable() +export class DeliverProcessorService { + #logger: Logger; + #suspendedHostsCache: Cache; + #latest: string | null; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private metaService: MetaService, + private utilityService: UtilityService, + private federatedInstanceService: FederatedInstanceService, + private fetchInstanceMetadataService: FetchInstanceMetadataService, + private apRequestService: ApRequestService, + private instanceChart: InstanceChart, + private apRequestChart: ApRequestChart, + private federationChart: FederationChart, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('deliver'); + this.#suspendedHostsCache = new Cache(1000 * 60 * 60); + this.#latest = null; + } + + public async process(job: Bull.Job): Promise { + const { host } = new URL(job.data.to); + + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (meta.blockedHosts.includes(this.utilityService.toPuny(host))) { + return 'skip (blocked)'; + } + + // isSuspendedなら中断 + let suspendedHosts = this.#suspendedHostsCache.get(null); + if (suspendedHosts == null) { + suspendedHosts = await this.instancesRepository.find({ + where: { + isSuspended: true, + }, + }); + this.#suspendedHostsCache.set(null, suspendedHosts); + } + if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) { + return 'skip (suspended)'; + } + + try { + if (this.#latest !== (this.#latest = JSON.stringify(job.data.content, null, 2))) { + this.#logger.debug(`delivering ${this.#latest}`); + } + + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); + + // Update stats + this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { + this.instancesRepository.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: 200, + lastCommunicatedAt: new Date(), + isNotResponding: false, + }); + + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + + this.instanceChart.requestSent(i.host, true); + this.apRequestChart.deliverSucc(); + this.federationChart.deliverd(i.host, true); + }); + + return 'Success'; + } catch (res) { + // Update stats + this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { + this.instancesRepository.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: res instanceof StatusError ? res.statusCode : null, + isNotResponding: true, + }); + + this.instanceChart.requestSent(i.host, false); + this.apRequestChart.deliverFail(); + this.federationChart.deliverd(i.host, false); + }); + + if (res instanceof StatusError) { + // 4xx + if (res.isClientError) { + // HTTPステータスコード4xxはクライアントエラーであり、それはつまり + // 何回再送しても成功することはないということなのでエラーにはしないでおく + return `${res.statusCode} ${res.statusMessage}`; + } + + // 5xx etc. + throw `${res.statusCode} ${res.statusMessage}`; + } else { + // DNS error, socket error, timeout ... + throw res; + } + } + } +} diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts new file mode 100644 index 0000000000..b90c7be629 --- /dev/null +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -0,0 +1,56 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { PollVotesRepository, NotesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { EndedPollNotificationJobData } from '../types.js'; + +@Injectable() +export class EndedPollNotificationProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + private createNotificationService: CreateNotificationService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + const note = await this.notesRepository.findOneBy({ id: job.data.noteId }); + if (note == null || !note.hasPoll) { + done(); + return; + } + + const votes = await this.pollVotesRepository.createQueryBuilder('vote') + .select('vote.userId') + .where('vote.noteId = :noteId', { noteId: note.id }) + .innerJoinAndSelect('vote.user', 'user') + .andWhere('user.host IS NULL') + .getMany(); + + const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; + + for (const userId of userIds) { + this.createNotificationService.createNotification(userId, 'pollEnded', { + noteId: note.id, + }); + } + + done(); + } +} diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts new file mode 100644 index 0000000000..9b520be06e --- /dev/null +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -0,0 +1,117 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { DriveFilesRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class ExportBlockingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private utilityService: UtilityService, + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-blocking'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Exporting blocking of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`Temp file is ${path}`); + + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); + + let exportedCount = 0; + let cursor: any = null; + + while (true) { + const blockings = await this.blockingsRepository.find({ + where: { + blockerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (blockings.length === 0) { + job.progress(100); + break; + } + + cursor = blockings[blockings.length - 1].id; + + for (const block of blockings) { + const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); + if (u == null) { + exportedCount++; continue; + } + + const content = this.utilityService.getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + exportedCount++; + } + + const total = await this.blockingsRepository.countBy({ + blockerId: user.id, + }); + + job.progress(exportedCount / total); + } + + stream.end(); + this.#logger.succ(`Exported to: ${path}`); + + const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + + done(); + } +} diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts new file mode 100644 index 0000000000..93341c2c63 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -0,0 +1,135 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { ulid } from 'ulid'; +import mime from 'mime-types'; +import archiver from 'archiver'; +import { DI } from '@/di-symbols.js'; +import { EmojisRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp, createTempDir } from '@/misc/create-temp.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; + +@Injectable() +export class ExportCustomEmojisProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private driveService: DriveService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info('Exporting custom emojis ...'); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const [path, cleanup] = await createTempDir(); + + this.#logger.info(`Temp dir is ${path}`); + + const metaPath = path + '/meta.json'; + + fs.writeFileSync(metaPath, '', 'utf-8'); + + const metaStream = fs.createWriteStream(metaPath, { flags: 'a' }); + + const writeMeta = (text: string): Promise => { + return new Promise((res, rej) => { + metaStream.write(text, err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + }; + + await writeMeta(`{"metaVersion":2,"host":"${this.config.host}","exportedAt":"${new Date().toString()}","emojis":[`); + + const customEmojis = await this.emojisRepository.find({ + where: { + host: IsNull(), + }, + order: { + id: 'ASC', + }, + }); + + for (const emoji of customEmojis) { + const ext = mime.extension(emoji.type); + const fileName = emoji.name + (ext ? '.' + ext : ''); + const emojiPath = path + '/' + fileName; + fs.writeFileSync(emojiPath, '', 'binary'); + let downloaded = false; + + try { + await this.downloadService.downloadUrl(emoji.originalUrl, emojiPath); + downloaded = true; + } catch (e) { // TODO: 何度か再試行 + this.#logger.error(e instanceof Error ? e : new Error(e as string)); + } + + if (!downloaded) { + fs.unlinkSync(emojiPath); + } + + const content = JSON.stringify({ + fileName: fileName, + downloaded: downloaded, + emoji: emoji, + }); + const isFirst = customEmojis.indexOf(emoji) === 0; + + await writeMeta(isFirst ? content : ',\n' + content); + } + + await writeMeta(']}'); + + metaStream.end(); + + // Create archive + const [archivePath, archiveCleanup] = await createTemp(); + const archiveStream = fs.createWriteStream(archivePath); + const archive = archiver('zip', { + zlib: { level: 0 }, + }); + archiveStream.on('close', async () => { + this.#logger.succ(`Exported to: ${archivePath}`); + + const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; + const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + cleanup(); + archiveCleanup(); + done(); + }); + archive.pipe(archiveStream); + archive.directory(path, false); + archive.finalize(); + } +} diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts new file mode 100644 index 0000000000..9946015ff7 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -0,0 +1,120 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { In, MoreThan, Not } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import { FollowingsRepository, MutingsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import type { Following } from '@/models/entities/Following.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class ExportFollowingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private utilityService: UtilityService, + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-following'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Exporting following of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`Temp file is ${path}`); + + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); + + let cursor: Following['id'] | null = null; + + const mutings = job.data.excludeMuting ? await this.mutingsRepository.findBy({ + muterId: user.id, + }) : []; + + while (true) { + const followings = await this.followingsRepository.find({ + where: { + followerId: user.id, + ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Following[]; + + if (followings.length === 0) { + break; + } + + cursor = followings[followings.length - 1].id; + + for (const following of followings) { + const u = await this.usersRepository.findOneBy({ id: following.followeeId }); + if (u == null) { + continue; + } + + if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { + continue; + } + + const content = this.utilityService.getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + } + } + + stream.end(); + this.#logger.succ(`Exported to: ${path}`); + + const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + + done(); + } +} diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts new file mode 100644 index 0000000000..a34cea0f41 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -0,0 +1,120 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class ExportMutingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private utilityService: UtilityService, + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-muting'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Exporting muting of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`Temp file is ${path}`); + + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); + + let exportedCount = 0; + let cursor: any = null; + + while (true) { + const mutes = await this.mutingsRepository.find({ + where: { + muterId: user.id, + expiresAt: IsNull(), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (mutes.length === 0) { + job.progress(100); + break; + } + + cursor = mutes[mutes.length - 1].id; + + for (const mute of mutes) { + const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); + if (u == null) { + exportedCount++; continue; + } + + const content = this.utilityService.getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + exportedCount++; + } + + const total = await this.mutingsRepository.countBy({ + muterId: user.id, + }); + + job.progress(exportedCount / total); + } + + stream.end(); + this.#logger.succ(`Exported to: ${path}`); + + const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + + done(); + } +} diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts new file mode 100644 index 0000000000..24fcc1a8ad --- /dev/null +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -0,0 +1,143 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import type { Poll } from '@/models/entities/Poll.js'; +import type { Note } from '@/models/entities/Note.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class ExportNotesProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-notes'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Exporting notes of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`Temp file is ${path}`); + + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); + + const write = (text: string): Promise => { + return new Promise((res, rej) => { + stream.write(text, err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + }; + + await write('['); + + let exportedNotesCount = 0; + let cursor: Note['id'] | null = null; + + while (true) { + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Note[]; + + if (notes.length === 0) { + job.progress(100); + break; + } + + cursor = notes[notes.length - 1].id; + + for (const note of notes) { + let poll: Poll | undefined; + if (note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); + } + const content = JSON.stringify(serialize(note, poll)); + const isFirst = exportedNotesCount === 0; + await write(isFirst ? content : ',\n' + content); + exportedNotesCount++; + } + + const total = await this.notesRepository.countBy({ + userId: user.id, + }); + + job.progress(exportedNotesCount / total); + } + + await write(']'); + + stream.end(); + this.#logger.succ(`Exported to: ${path}`); + + const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + + done(); + } +} + +function serialize(note: Note, poll: Poll | null = null): Record { + return { + id: note.id, + text: note.text, + createdAt: note.createdAt, + fileIds: note.fileIds, + replyId: note.replyId, + renoteId: note.renoteId, + poll: poll, + cw: note.cw, + visibility: note.visibility, + visibleUserIds: note.visibleUserIds, + localOnly: note.localOnly, + }; +} diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts new file mode 100644 index 0000000000..a02e9bdee4 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -0,0 +1,96 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { In, IsNull, MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserJobData } from '../types.js'; + +@Injectable() +export class ExportUserListsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private utilityService: UtilityService, + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('export-user-lists'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Exporting user lists of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const lists = await this.userListsRepository.findBy({ + userId: user.id, + }); + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.#logger.info(`Temp file is ${path}`); + + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); + + for (const list of lists) { + const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id }); + const users = await this.usersRepository.findBy({ + id: In(joinings.map(j => j.userId)), + }); + + for (const u of users) { + const acct = this.utilityService.getFullApAccount(u.username, u.host); + const content = `${list.name},${acct}`; + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + this.#logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + } + } + + stream.end(); + this.#logger.succ(`Exported to: ${path}`); + + const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true }); + + this.#logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + + done(); + } +} diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts new file mode 100644 index 0000000000..abae196299 --- /dev/null +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { BlockingsRepository, DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import * as Acct from '@/misc/acct.js'; +import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserImportJobData } from '../types.js'; + +@Injectable() +export class ImportBlockingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private utilityService: UtilityService, + private userBlockingService: UserBlockingService, + private resolveUserService: ResolveUserService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('import-blocking'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Importing blocking of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const file = await this.driveFilesRepository.findOneBy({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + const csv = await this.downloadService.downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = Acct.parse(acct); + + let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host!), + usernameLower: username.toLowerCase(), + }); + + if (host == null && target == null) continue; + + if (target == null) { + target = await this.resolveUserService.resolveUser(username, host); + } + + if (target == null) { + throw `cannot resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) continue; + + this.#logger.info(`Block[${linenum}] ${target.id} ...`); + + await this.userBlockingService.block(user, target); + } catch (e) { + this.#logger.warn(`Error in line:${linenum} ${e}`); + } + } + + this.#logger.succ('Imported'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts new file mode 100644 index 0000000000..6f86589aec --- /dev/null +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -0,0 +1,110 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan, DataSource } from 'typeorm'; +import unzipper from 'unzipper'; +import { DI } from '@/di-symbols.js'; +import { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +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 { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserImportJobData } from '../types.js'; + +// TODO: 名前衝突時の動作を選べるようにする +@Injectable() +export class ImportCustomEmojisProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private customEmojiService: CustomEmojiService, + private driveService: DriveService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('import-custom-emojis'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info('Importing custom emojis ...'); + + const file = await this.driveFilesRepository.findOneBy({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + const [path, cleanup] = await createTempDir(); + + this.#logger.info(`Temp dir is ${path}`); + + const destPath = path + '/emojis.zip'; + + try { + fs.writeFileSync(destPath, '', 'binary'); + await this.downloadService.downloadUrl(file.url, destPath); + } catch (e) { // TODO: 何度か再試行 + if (e instanceof Error || typeof e === 'string') { + this.#logger.error(e); + } + throw e; + } + + const outputPath = path + '/emojis'; + const unzipStream = fs.createReadStream(destPath); + const extractor = unzipper.Extract({ path: outputPath }); + extractor.on('close', async () => { + const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); + const meta = JSON.parse(metaRaw); + + for (const record of meta.emojis) { + if (!record.downloaded) continue; + const emojiInfo = record.emoji; + const emojiPath = outputPath + '/' + record.fileName; + await this.emojisRepository.delete({ + name: emojiInfo.name, + }); + const driveFile = await this.driveService.addFile({ + user: null, + path: emojiPath, + name: record.fileName, + force: true, + }); + await this.customEmojiService.add({ + name: emojiInfo.name, + category: emojiInfo.category, + host: null, + aliases: emojiInfo.aliases, + driveFile, + }); + } + + cleanup(); + + this.#logger.succ('Imported'); + done(); + }); + unzipStream.pipe(extractor); + this.#logger.succ(`Unzipping to ${outputPath}`); + } +} diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts new file mode 100644 index 0000000000..087e0baf96 --- /dev/null +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import * as Acct from '@/misc/acct.js'; +import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserImportJobData } from '../types.js'; + +@Injectable() +export class ImportFollowingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private utilityService: UtilityService, + private userFollowingService: UserFollowingService, + private resolveUserService: ResolveUserService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('import-following'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Importing following of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const file = await this.driveFilesRepository.findOneBy({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + const csv = await this.downloadService.downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = Acct.parse(acct); + + let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host!), + usernameLower: username.toLowerCase(), + }); + + if (host == null && target == null) continue; + + if (target == null) { + target = await this.resolveUserService.resolveUser(username, host); + } + + if (target == null) { + throw `cannot resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) continue; + + this.#logger.info(`Follow[${linenum}] ${target.id} ...`); + + this.userFollowingService.follow(user, target); + } catch (e) { + this.#logger.warn(`Error in line:${linenum} ${e}`); + } + } + + this.#logger.succ('Imported'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts new file mode 100644 index 0000000000..404091e8ca --- /dev/null +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -0,0 +1,100 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import * as Acct from '@/misc/acct.js'; +import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import type { UserFollowingService } from '@/core/UserFollowingService.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserImportJobData } from '../types.js'; + +@Injectable() +export class ImportMutingProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private utilityService: UtilityService, + private userMutingService: UserMutingService, + private resolveUserService: ResolveUserService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('import-muting'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Importing muting of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const file = await this.driveFilesRepository.findOneBy({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + const csv = await this.downloadService.downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = Acct.parse(acct); + + let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host!), + usernameLower: username.toLowerCase(), + }); + + if (host == null && target == null) continue; + + if (target == null) { + target = await this.resolveUserService.resolveUser(username, host); + } + + if (target == null) { + throw `cannot resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) continue; + + this.#logger.info(`Mute[${linenum}] ${target.id} ...`); + + await this.userMutingService.mute(user, target); + } catch (e) { + this.#logger.warn(`Error in line:${linenum} ${e}`); + } + } + + this.#logger.succ('Imported'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts new file mode 100644 index 0000000000..aed1a4cde5 --- /dev/null +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -0,0 +1,112 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import * as Acct from '@/misc/acct.js'; +import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { UserListService } from '@/core/UserListService.js'; +import { IdService } from '@/core/IdService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DbUserImportJobData } from '../types.js'; + +@Injectable() +export class ImportUserListsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private utilityService: UtilityService, + private idService: IdService, + private userListService: UserListService, + private resolveUserService: ResolveUserService, + private downloadService: DownloadService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('import-user-lists'); + } + + public async process(job: Bull.Job, done: () => void): Promise { + this.#logger.info(`Importing user lists of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + + const file = await this.driveFilesRepository.findOneBy({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + const csv = await this.downloadService.downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const listName = line.split(',')[0].trim(); + const { username, host } = Acct.parse(line.split(',')[1].trim()); + + let list = await this.userListsRepository.findOneBy({ + userId: user.id, + name: listName, + }); + + if (list == null) { + list = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: user.id, + name: listName, + }).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + } + + let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host!), + usernameLower: username.toLowerCase(), + }); + + if (target == null) { + target = await this.resolveUserService.resolveUser(username, host); + } + + if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; + + this.userListService.push(target, list!); + } catch (e) { + this.#logger.warn(`Error in line:${linenum} ${e}`); + } + } + + this.#logger.succ('Imported'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts new file mode 100644 index 0000000000..5733f5d0a9 --- /dev/null +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -0,0 +1,195 @@ +import { URL } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import httpSignature from '@peertube/http-signature'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApRequestService } from '@/core/remote/activitypub/ApRequestService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { Cache } from '@/misc/cache.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import ApRequestChart from '@/core/chart/charts/ap-request.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import { getApId } from '@/core/remote/activitypub/type.js'; +import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { ApDbResolverService } from '@/core/remote/activitypub/ApDbResolverService.js'; +import { StatusError } from '@/misc/status-error.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { ApPersonService } from '@/core/remote/activitypub/models/ApPersonService.js'; +import { LdSignatureService } from '@/core/remote/activitypub/LdSignatureService.js'; +import { ApInboxService } from '@/core/remote/activitypub/ApInboxService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { DeliverJobData, InboxJobData } from '../types.js'; + +// ユーザーのinboxにアクティビティが届いた時の処理 +@Injectable() +export class InboxProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private utilityService: UtilityService, + private metaService: MetaService, + private apInboxService: ApInboxService, + private federatedInstanceService: FederatedInstanceService, + private fetchInstanceMetadataService: FetchInstanceMetadataService, + private ldSignatureService: LdSignatureService, + private apRequestService: ApRequestService, + private apPersonService: ApPersonService, + private apDbResolverService: ApDbResolverService, + private instanceChart: InstanceChart, + private apRequestChart: ApRequestChart, + private federationChart: FederationChart, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('inbox'); + } + + public async process(job: Bull.Job): Promise { + const signature = job.data.signature; // HTTP-signature + const activity = job.data.activity; + + //#region Log + const info = Object.assign({}, activity) as any; + delete info['@context']; + this.#logger.debug(JSON.stringify(info, null, 2)); + //#endregion + + const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); + + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (meta.blockedHosts.includes(host)) { + return `Blocked request: ${host}`; + } + + const keyIdLower = signature.keyId.toLowerCase(); + if (keyIdLower.startsWith('acct:')) { + return `Old keyId is no longer supported. ${keyIdLower}`; + } + + // HTTP-Signature keyIdを元にDBから取得 + let authUser: { + user: CacheableRemoteUser; + key: UserPublickey | null; + } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); + + // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 + if (authUser == null) { + try { + authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor)); + } catch (err) { + // 対象が4xxならスキップ + if (err instanceof StatusError) { + if (err.isClientError) { + return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`; + } + throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`; + } + } + } + + // それでもわからなければ終了 + if (authUser == null) { + return 'skip: failed to resolve user'; + } + + // publicKey がなくても終了 + if (authUser.key == null) { + return 'skip: failed to resolve user publicKey'; + } + + // HTTP-Signatureの検証 + const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + + // また、signatureのsignerは、activity.actorと一致する必要がある + if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { + // 一致しなくても、でもLD-Signatureがありそうならそっちも見る + if (activity.signature) { + if (activity.signature.type !== 'RsaSignature2017') { + return `skip: unsupported LD-signature type ${activity.signature.type}`; + } + + // activity.signature.creator: https://example.oom/users/user#main-key + // みたいになっててUserを引っ張れば公開キーも入ることを期待する + if (activity.signature.creator) { + const candicate = activity.signature.creator.replace(/#.*/, ''); + await this.apPersonService.resolvePerson(candicate).catch(() => null); + } + + // keyIdからLD-Signatureのユーザーを取得 + authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); + if (authUser == null) { + return 'skip: LD-Signatureのユーザーが取得できませんでした'; + } + + if (authUser.key == null) { + return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'; + } + + // LD-Signature検証 + const ldSignature = this.ldSignatureService.use(); + const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); + if (!verified) { + return 'skip: LD-Signatureの検証に失敗しました'; + } + + // もう一度actorチェック + if (authUser.user.uri !== activity.actor) { + return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; + } + + // ブロックしてたら中断 + const ldHost = this.utilityService.extractDbHost(authUser.user.uri); + if (meta.blockedHosts.includes(ldHost)) { + return `Blocked request: ${ldHost}`; + } + } else { + return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`; + } + } + + // activity.idがあればホストが署名者のホストであることを確認する + if (typeof activity.id === 'string') { + const signerHost = this.utilityService.extractDbHost(authUser.user.uri!); + const activityIdHost = this.utilityService.extractDbHost(activity.id); + if (signerHost !== activityIdHost) { + return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; + } + } + + // Update stats + this.federatedInstanceService.registerOrFetchInstanceDoc(authUser.user.host).then(i => { + this.instancesRepository.update(i.id, { + latestRequestReceivedAt: new Date(), + lastCommunicatedAt: new Date(), + isNotResponding: false, + }); + + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + + this.instanceChart.requestReceived(i.host); + this.apRequestChart.inbox(); + this.federationChart.inbox(i.host); + }); + + // アクティビティを処理 + await this.apInboxService.performActivity(authUser.user, activity); + return 'ok'; + } +} diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts new file mode 100644 index 0000000000..f976232a24 --- /dev/null +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -0,0 +1,61 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import DriveChart from '@/core/chart/charts/drive.js'; +import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; +import HashtagChart from '@/core/chart/charts/hashtag.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +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'; + +@Injectable() +export class ResyncChartsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private federationChart: FederationChart, + private notesChart: NotesChart, + private usersChart: UsersChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + private perUserNotesChart: PerUserNotesChart, + private driveChart: DriveChart, + private perUserReactionsChart: PerUserReactionsChart, + private hashtagChart: HashtagChart, + private perUserFollowingChart: PerUserFollowingChart, + private perUserDriveChart: PerUserDriveChart, + private apRequestChart: ApRequestChart, + + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('resync-charts'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Resync charts...'); + + // TODO: ユーザーごとのチャートも更新する + // TODO: インスタンスごとのチャートも更新する + await Promise.all([ + this.driveChart.resync(), + this.notesChart.resync(), + this.usersChart.resync(), + ]); + + this.#logger.succ('All charts successfully resynced.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts new file mode 100644 index 0000000000..d1ca3c4576 --- /dev/null +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import DriveChart from '@/core/chart/charts/drive.js'; +import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; +import HashtagChart from '@/core/chart/charts/hashtag.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +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'; + +@Injectable() +export class TickChartsProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private federationChart: FederationChart, + private notesChart: NotesChart, + private usersChart: UsersChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + private perUserNotesChart: PerUserNotesChart, + private driveChart: DriveChart, + private perUserReactionsChart: PerUserReactionsChart, + private hashtagChart: HashtagChart, + private perUserFollowingChart: PerUserFollowingChart, + private perUserDriveChart: PerUserDriveChart, + private apRequestChart: ApRequestChart, + + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('tick-charts'); + } + + public async process(job: Bull.Job>, done: () => void): Promise { + this.#logger.info('Tick charts...'); + + await Promise.all([ + this.federationChart.tick(false), + this.notesChart.tick(false), + this.usersChart.tick(false), + this.activeUsersChart.tick(false), + this.instanceChart.tick(false), + this.perUserNotesChart.tick(false), + this.driveChart.tick(false), + this.perUserReactionsChart.tick(false), + this.hashtagChart.tick(false), + this.perUserFollowingChart.tick(false), + this.perUserDriveChart.tick(false), + this.apRequestChart.tick(false), + ]); + + this.#logger.succ('All charts successfully ticked.'); + done(); + } +} diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts new file mode 100644 index 0000000000..5723fe2eeb --- /dev/null +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -0,0 +1,79 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { StatusError } from '@/misc/status-error.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type Bull from 'bull'; +import type { WebhookDeliverJobData } from '../types.js'; + +@Injectable() +export class WebhookDeliverProcessorService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + private httpRequestService: HttpRequestService, + private queueLoggerService: QueueLoggerService, + ) { + this.#logger = this.queueLoggerService.logger.createSubLogger('webhook'); + } + + public async process(job: Bull.Job): Promise { + try { + this.#logger.debug(`delivering ${job.data.webhookId}`); + + const res = await this.httpRequestService.getResponse({ + url: job.data.to, + method: 'POST', + headers: { + 'User-Agent': 'Misskey-Hooks', + 'X-Misskey-Host': this.config.host, + 'X-Misskey-Hook-Id': job.data.webhookId, + 'X-Misskey-Hook-Secret': job.data.secret, + }, + body: JSON.stringify({ + hookId: job.data.webhookId, + userId: job.data.userId, + eventId: job.data.eventId, + createdAt: job.data.createdAt, + type: job.data.type, + body: job.data.content, + }), + }); + + this.webhooksRepository.update({ id: job.data.webhookId }, { + latestSentAt: new Date(), + latestStatus: res.status, + }); + + return 'Success'; + } catch (res) { + this.webhooksRepository.update({ id: job.data.webhookId }, { + latestSentAt: new Date(), + latestStatus: res instanceof StatusError ? res.statusCode : 1, + }); + + if (res instanceof StatusError) { + // 4xx + if (res.isClientError) { + return `${res.statusCode} ${res.statusMessage}`; + } + + // 5xx etc. + throw `${res.statusCode} ${res.statusMessage}`; + } else { + // DNS error, socket error, timeout ... + throw res; + } + } + } +} diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts deleted file mode 100644 index c1657b4be6..0000000000 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ /dev/null @@ -1,94 +0,0 @@ -import Bull from 'bull'; -import { queueLogger } from '../../logger.js'; -import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index.js'; -import { DbUserDeleteJobData } from '@/queue/types.js'; -import { Note } from '@/models/entities/note.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { MoreThan } from 'typeorm'; -import { deleteFileSync } from '@/services/drive/delete-file.js'; -import { sendEmail } from '@/services/send-email.js'; - -const logger = queueLogger.createSubLogger('delete-account'); - -export async function deleteAccount(job: Bull.Job): Promise { - logger.info(`Deleting account of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - return; - } - - { // Delete notes - let cursor: Note['id'] | null = null; - - while (true) { - const notes = await Notes.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Note[]; - - if (notes.length === 0) { - break; - } - - cursor = notes[notes.length - 1].id; - - await Notes.delete(notes.map(note => note.id)); - } - - logger.succ(`All of notes deleted`); - } - - { // Delete files - let cursor: DriveFile['id'] | null = null; - - while (true) { - const files = await DriveFiles.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 10, - order: { - id: 1, - }, - }) as DriveFile[]; - - if (files.length === 0) { - break; - } - - cursor = files[files.length - 1].id; - - for (const file of files) { - await deleteFileSync(file); - } - } - - logger.succ(`All of files deleted`); - } - - { // Send email notification - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - if (profile.email && profile.emailVerified) { - sendEmail(profile.email, 'Account deleted', - `Your account has been deleted.`, - `Your account has been deleted.`); - } - } - - // soft指定されている場合は物理削除しない - if (job.data.soft) { - // nop - } else { - await Users.delete(job.data.user.id); - } - - return 'Account deleted'; -} diff --git a/packages/backend/src/queue/processors/db/delete-drive-files.ts b/packages/backend/src/queue/processors/db/delete-drive-files.ts deleted file mode 100644 index b3832d9f04..0000000000 --- a/packages/backend/src/queue/processors/db/delete-drive-files.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import { deleteFileSync } from '@/services/drive/delete-file.js'; -import { Users, DriveFiles } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; -import { DbUserJobData } from '@/queue/types.js'; - -const logger = queueLogger.createSubLogger('delete-drive-files'); - -export async function deleteDriveFiles(job: Bull.Job, done: any): Promise { - logger.info(`Deleting drive files of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - let deletedCount = 0; - let cursor: any = null; - - while (true) { - const files = await DriveFiles.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); - - if (files.length === 0) { - job.progress(100); - break; - } - - cursor = files[files.length - 1].id; - - for (const file of files) { - await deleteFileSync(file); - deletedCount++; - } - - const total = await DriveFiles.countBy({ - userId: user.id, - }); - - job.progress(deletedCount / total); - } - - logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts deleted file mode 100644 index f5e0424a79..0000000000 --- a/packages/backend/src/queue/processors/db/export-blocking.ts +++ /dev/null @@ -1,93 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, Blockings } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; -import { DbUserJobData } from '@/queue/types.js'; - -const logger = queueLogger.createSubLogger('export-blocking'); - -export async function exportBlocking(job: Bull.Job, done: any): Promise { - logger.info(`Exporting blocking of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: 'a' }); - - let exportedCount = 0; - let cursor: any = null; - - while (true) { - const blockings = await Blockings.find({ - where: { - blockerId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); - - if (blockings.length === 0) { - job.progress(100); - break; - } - - cursor = blockings[blockings.length - 1].id; - - for (const block of blockings) { - const u = await Users.findOneBy({ id: block.blockeeId }); - if (u == null) { - exportedCount++; continue; - } - - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - exportedCount++; - } - - const total = await Blockings.countBy({ - blockerId: user.id, - }); - - job.progress(exportedCount / total); - } - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts deleted file mode 100644 index 3da887cda2..0000000000 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ /dev/null @@ -1,114 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { ulid } from 'ulid'; -import mime from 'mime-types'; -import archiver from 'archiver'; -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { Users, Emojis } from '@/models/index.js'; -import { } from '@/queue/types.js'; -import { createTemp, createTempDir } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import config from '@/config/index.js'; -import { IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('export-custom-emojis'); - -export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promise { - logger.info(`Exporting custom emojis ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const [path, cleanup] = await createTempDir(); - - logger.info(`Temp dir is ${path}`); - - const metaPath = path + '/meta.json'; - - fs.writeFileSync(metaPath, '', 'utf-8'); - - const metaStream = fs.createWriteStream(metaPath, { flags: 'a' }); - - const writeMeta = (text: string): Promise => { - return new Promise((res, rej) => { - metaStream.write(text, err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - }; - - await writeMeta(`{"metaVersion":2,"host":"${config.host}","exportedAt":"${new Date().toString()}","emojis":[`); - - const customEmojis = await Emojis.find({ - where: { - host: IsNull(), - }, - order: { - id: 'ASC', - }, - }); - - for (const emoji of customEmojis) { - const ext = mime.extension(emoji.type); - const fileName = emoji.name + (ext ? '.' + ext : ''); - const emojiPath = path + '/' + fileName; - fs.writeFileSync(emojiPath, '', 'binary'); - let downloaded = false; - - try { - await downloadUrl(emoji.originalUrl, emojiPath); - downloaded = true; - } catch (e) { // TODO: 何度か再試行 - logger.error(e instanceof Error ? e : new Error(e as string)); - } - - if (!downloaded) { - fs.unlinkSync(emojiPath); - } - - const content = JSON.stringify({ - fileName: fileName, - downloaded: downloaded, - emoji: emoji, - }); - const isFirst = customEmojis.indexOf(emoji) === 0; - - await writeMeta(isFirst ? content : ',\n' + content); - } - - await writeMeta(']}'); - - metaStream.end(); - - // Create archive - const [archivePath, archiveCleanup] = await createTemp(); - const archiveStream = fs.createWriteStream(archivePath); - const archive = archiver('zip', { - zlib: { level: 0 }, - }); - archiveStream.on('close', async () => { - logger.succ(`Exported to: ${archivePath}`); - - const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; - const driveFile = await addFile({ user, path: archivePath, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); - archiveCleanup(); - done(); - }); - archive.pipe(archiveStream); - archive.directory(path, false); - archive.finalize(); -} diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts deleted file mode 100644 index 4ac165567b..0000000000 --- a/packages/backend/src/queue/processors/db/export-following.ts +++ /dev/null @@ -1,94 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, Followings, Mutings } from '@/models/index.js'; -import { In, MoreThan, Not } from 'typeorm'; -import { DbUserJobData } from '@/queue/types.js'; -import { Following } from '@/models/entities/following.js'; - -const logger = queueLogger.createSubLogger('export-following'); - -export async function exportFollowing(job: Bull.Job, done: () => void): Promise { - logger.info(`Exporting following of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: 'a' }); - - let cursor: Following['id'] | null = null; - - const mutings = job.data.excludeMuting ? await Mutings.findBy({ - muterId: user.id, - }) : []; - - while (true) { - const followings = await Followings.find({ - where: { - followerId: user.id, - ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Following[]; - - if (followings.length === 0) { - break; - } - - cursor = followings[followings.length - 1].id; - - for (const following of followings) { - const u = await Users.findOneBy({ id: following.followeeId }); - if (u == null) { - continue; - } - - if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { - continue; - } - - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - } - } - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts deleted file mode 100644 index 6a36cfa072..0000000000 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ /dev/null @@ -1,94 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, Mutings } from '@/models/index.js'; -import { IsNull, MoreThan } from 'typeorm'; -import { DbUserJobData } from '@/queue/types.js'; - -const logger = queueLogger.createSubLogger('export-mute'); - -export async function exportMute(job: Bull.Job, done: any): Promise { - logger.info(`Exporting mute of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: 'a' }); - - let exportedCount = 0; - let cursor: any = null; - - while (true) { - const mutes = await Mutings.find({ - where: { - muterId: user.id, - expiresAt: IsNull(), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); - - if (mutes.length === 0) { - job.progress(100); - break; - } - - cursor = mutes[mutes.length - 1].id; - - for (const mute of mutes) { - const u = await Users.findOneBy({ id: mute.muteeId }); - if (u == null) { - exportedCount++; continue; - } - - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - exportedCount++; - } - - const total = await Mutings.countBy({ - muterId: user.id, - }); - - job.progress(exportedCount / total); - } - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts deleted file mode 100644 index 051fcdf385..0000000000 --- a/packages/backend/src/queue/processors/db/export-notes.ts +++ /dev/null @@ -1,118 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { Users, Notes, Polls } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; -import { Note } from '@/models/entities/note.js'; -import { Poll } from '@/models/entities/poll.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { createTemp } from '@/misc/create-temp.js'; - -const logger = queueLogger.createSubLogger('export-notes'); - -export async function exportNotes(job: Bull.Job, done: any): Promise { - logger.info(`Exporting notes of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: 'a' }); - - const write = (text: string): Promise => { - return new Promise((res, rej) => { - stream.write(text, err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - }; - - await write('['); - - let exportedNotesCount = 0; - let cursor: Note['id'] | null = null; - - while (true) { - const notes = await Notes.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Note[]; - - if (notes.length === 0) { - job.progress(100); - break; - } - - cursor = notes[notes.length - 1].id; - - for (const note of notes) { - let poll: Poll | undefined; - if (note.hasPoll) { - poll = await Polls.findOneByOrFail({ noteId: note.id }); - } - const content = JSON.stringify(serialize(note, poll)); - const isFirst = exportedNotesCount === 0; - await write(isFirst ? content : ',\n' + content); - exportedNotesCount++; - } - - const total = await Notes.countBy({ - userId: user.id, - }); - - job.progress(exportedNotesCount / total); - } - - await write(']'); - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} - -function serialize(note: Note, poll: Poll | null = null): Record { - return { - id: note.id, - text: note.text, - createdAt: note.createdAt, - fileIds: note.fileIds, - replyId: note.replyId, - renoteId: note.renoteId, - poll: poll, - cw: note.cw, - visibility: note.visibility, - visibleUserIds: note.visibleUserIds, - localOnly: note.localOnly, - }; -} diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts deleted file mode 100644 index 71dd72df27..0000000000 --- a/packages/backend/src/queue/processors/db/export-user-lists.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; - -import { queueLogger } from '../../logger.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { format as dateFormat } from 'date-fns'; -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, UserLists, UserListJoinings } from '@/models/index.js'; -import { In } from 'typeorm'; -import { DbUserJobData } from '@/queue/types.js'; - -const logger = queueLogger.createSubLogger('export-user-lists'); - -export async function exportUserLists(job: Bull.Job, done: any): Promise { - logger.info(`Exporting user lists of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const lists = await UserLists.findBy({ - userId: user.id, - }); - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: 'a' }); - - for (const list of lists) { - const joinings = await UserListJoinings.findBy({ userListId: list.id }); - const users = await Users.findBy({ - id: In(joinings.map(j => j.userId)), - }); - - for (const u of users) { - const acct = getFullApAccount(u.username, u.host); - const content = `${list.name},${acct}`; - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - } - } - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-blocking.ts b/packages/backend/src/queue/processors/db/import-blocking.ts deleted file mode 100644 index 8bddf34bc2..0000000000 --- a/packages/backend/src/queue/processors/db/import-blocking.ts +++ /dev/null @@ -1,75 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import * as Acct from '@/misc/acct.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { Users, DriveFiles, Blockings } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import block from '@/services/blocking/create.js'; -import { IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('import-blocking'); - -export async function importBlocking(job: Bull.Job, done: any): Promise { - logger.info(`Importing blocking of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const acct = line.split(',')[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw `cannot resolve user: @${username}@${host}`; - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Block[${linenum}] ${target.id} ...`); - - await block(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ('Imported'); - done(); -} - diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts deleted file mode 100644 index 64dfe85374..0000000000 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ /dev/null @@ -1,81 +0,0 @@ -import Bull from 'bull'; -import * as fs from 'node:fs'; -import unzipper from 'unzipper'; - -import { queueLogger } from '../../logger.js'; -import { createTempDir } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { DriveFiles, Emojis } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { genId } from '@/misc/gen-id.js'; -import { db } from '@/db/postgre.js'; - -const logger = queueLogger.createSubLogger('import-custom-emojis'); - -// TODO: 名前衝突時の動作を選べるようにする -export async function importCustomEmojis(job: Bull.Job, done: any): Promise { - logger.info(`Importing custom emojis ...`); - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const [path, cleanup] = await createTempDir(); - - logger.info(`Temp dir is ${path}`); - - const destPath = path + '/emojis.zip'; - - try { - fs.writeFileSync(destPath, '', 'binary'); - await downloadUrl(file.url, destPath); - } catch (e) { // TODO: 何度か再試行 - if (e instanceof Error || typeof e === 'string') { - logger.error(e); - } - throw e; - } - - const outputPath = path + '/emojis'; - const unzipStream = fs.createReadStream(destPath); - const extractor = unzipper.Extract({ path: outputPath }); - extractor.on('close', async () => { - const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); - const meta = JSON.parse(metaRaw); - - for (const record of meta.emojis) { - if (!record.downloaded) continue; - const emojiInfo = record.emoji; - const emojiPath = outputPath + '/' + record.fileName; - await Emojis.delete({ - name: emojiInfo.name, - }); - const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true }); - const emoji = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emojiInfo.name, - category: emojiInfo.category, - host: null, - aliases: emojiInfo.aliases, - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, - }).then(x => Emojis.findOneByOrFail(x.identifiers[0])); - } - - await db.queryResultCache!.remove(['meta_emojis']); - - cleanup(); - - logger.succ('Imported'); - done(); - }); - unzipStream.pipe(extractor); - logger.succ(`Unzipping to ${outputPath}`); -} diff --git a/packages/backend/src/queue/processors/db/import-following.ts b/packages/backend/src/queue/processors/db/import-following.ts deleted file mode 100644 index 8ce2c367d6..0000000000 --- a/packages/backend/src/queue/processors/db/import-following.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import follow from '@/services/following/create.js'; -import * as Acct from '@/misc/acct.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { Users, DriveFiles } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('import-following'); - -export async function importFollowing(job: Bull.Job, done: any): Promise { - logger.info(`Importing following of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const acct = line.split(',')[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw `cannot resolve user: @${username}@${host}`; - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Follow[${linenum}] ${target.id} ...`); - - follow(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ('Imported'); - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-muting.ts b/packages/backend/src/queue/processors/db/import-muting.ts deleted file mode 100644 index 8552b797be..0000000000 --- a/packages/backend/src/queue/processors/db/import-muting.ts +++ /dev/null @@ -1,84 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import * as Acct from '@/misc/acct.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { Users, DriveFiles, Mutings } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { User } from '@/models/entities/user.js'; -import { genId } from '@/misc/gen-id.js'; -import { IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('import-muting'); - -export async function importMuting(job: Bull.Job, done: any): Promise { - logger.info(`Importing muting of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const acct = line.split(',')[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw `cannot resolve user: @${username}@${host}`; - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Mute[${linenum}] ${target.id} ...`); - - await mute(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ('Imported'); - done(); -} - -async function mute(user: User, target: User) { - await Mutings.insert({ - id: genId(), - createdAt: new Date(), - muterId: user.id, - muteeId: target.id, - }); -} diff --git a/packages/backend/src/queue/processors/db/import-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts deleted file mode 100644 index 9919b7c53c..0000000000 --- a/packages/backend/src/queue/processors/db/import-user-lists.ts +++ /dev/null @@ -1,80 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import * as Acct from '@/misc/acct.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { pushUserToUserList } from '@/services/user-list/push.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('import-user-lists'); - -export async function importUserLists(job: Bull.Job, done: any): Promise { - logger.info(`Importing user lists of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const listName = line.split(',')[0].trim(); - const { username, host } = Acct.parse(line.split(',')[1].trim()); - - let list = await UserLists.findOneBy({ - userId: user.id, - name: listName, - }); - - if (list == null) { - list = await UserLists.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: listName, - }).then(x => UserLists.findOneByOrFail(x.identifiers[0])); - } - - let target = isSelfHost(host!) ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (target == null) { - target = await resolveUser(username, host); - } - - if (await UserListJoinings.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; - - pushUserToUserList(target, list!); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ('Imported'); - done(); -} diff --git a/packages/backend/src/queue/processors/db/index.ts b/packages/backend/src/queue/processors/db/index.ts deleted file mode 100644 index e91d569779..0000000000 --- a/packages/backend/src/queue/processors/db/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Bull from 'bull'; -import { DbJobData } from '@/queue/types.js'; -import { deleteDriveFiles } from './delete-drive-files.js'; -import { exportCustomEmojis } from './export-custom-emojis.js'; -import { exportNotes } from './export-notes.js'; -import { exportFollowing } from './export-following.js'; -import { exportMute } from './export-mute.js'; -import { exportBlocking } from './export-blocking.js'; -import { exportUserLists } from './export-user-lists.js'; -import { importFollowing } from './import-following.js'; -import { importUserLists } from './import-user-lists.js'; -import { deleteAccount } from './delete-account.js'; -import { importMuting } from './import-muting.js'; -import { importBlocking } from './import-blocking.js'; -import { importCustomEmojis } from './import-custom-emojis.js'; - -const jobs = { - deleteDriveFiles, - exportCustomEmojis, - exportNotes, - exportFollowing, - exportMute, - exportBlocking, - exportUserLists, - importFollowing, - importMuting, - importBlocking, - importUserLists, - importCustomEmojis, - deleteAccount, -} as Record | Bull.ProcessPromiseFunction>; - -export default function(dbQueue: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts deleted file mode 100644 index 291c05766e..0000000000 --- a/packages/backend/src/queue/processors/deliver.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { URL } from 'node:url'; -import Bull from 'bull'; -import request from '@/remote/activitypub/request.js'; -import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import Logger from '@/services/logger.js'; -import { Instances } from '@/models/index.js'; -import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { Cache } from '@/misc/cache.js'; -import { Instance } from '@/models/entities/instance.js'; -import { DeliverJobData } from '../types.js'; -import { StatusError } from '@/misc/fetch.js'; - -const logger = new Logger('deliver'); - -let latest: string | null = null; - -const suspendedHostsCache = new Cache(1000 * 60 * 60); - -export default async (job: Bull.Job) => { - const { host } = new URL(job.data.to); - - // ブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(toPuny(host))) { - return 'skip (blocked)'; - } - - // isSuspendedなら中断 - let suspendedHosts = suspendedHostsCache.get(null); - if (suspendedHosts == null) { - suspendedHosts = await Instances.find({ - where: { - isSuspended: true, - }, - }); - suspendedHostsCache.set(null, suspendedHosts); - } - if (suspendedHosts.map(x => x.host).includes(toPuny(host))) { - return 'skip (suspended)'; - } - - try { - if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) { - logger.debug(`delivering ${latest}`); - } - - await request(job.data.user, job.data.to, job.data.content); - - // Update stats - registerOrFetchInstanceDoc(host).then(i => { - Instances.update(i.id, { - latestRequestSentAt: new Date(), - latestStatus: 200, - lastCommunicatedAt: new Date(), - isNotResponding: false, - }); - - fetchInstanceMetadata(i); - - instanceChart.requestSent(i.host, true); - apRequestChart.deliverSucc(); - federationChart.deliverd(i.host, true); - }); - - return 'Success'; - } catch (res) { - // Update stats - registerOrFetchInstanceDoc(host).then(i => { - Instances.update(i.id, { - latestRequestSentAt: new Date(), - latestStatus: res instanceof StatusError ? res.statusCode : null, - isNotResponding: true, - }); - - instanceChart.requestSent(i.host, false); - apRequestChart.deliverFail(); - federationChart.deliverd(i.host, false); - }); - - if (res instanceof StatusError) { - // 4xx - if (res.isClientError) { - // HTTPステータスコード4xxはクライアントエラーであり、それはつまり - // 何回再送しても成功することはないということなのでエラーにはしないでおく - return `${res.statusCode} ${res.statusMessage}`; - } - - // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; - } else { - // DNS error, socket error, timeout ... - throw res; - } - } -}; diff --git a/packages/backend/src/queue/processors/ended-poll-notification.ts b/packages/backend/src/queue/processors/ended-poll-notification.ts deleted file mode 100644 index 6151c96ad6..0000000000 --- a/packages/backend/src/queue/processors/ended-poll-notification.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Bull from 'bull'; -import { In } from 'typeorm'; -import { Notes, Polls, PollVotes } from '@/models/index.js'; -import { queueLogger } from '../logger.js'; -import { EndedPollNotificationJobData } from '@/queue/types.js'; -import { createNotification } from '@/services/create-notification.js'; - -const logger = queueLogger.createSubLogger('ended-poll-notification'); - -export async function endedPollNotification(job: Bull.Job, done: any): Promise { - const note = await Notes.findOneBy({ id: job.data.noteId }); - if (note == null || !note.hasPoll) { - done(); - return; - } - - const votes = await PollVotes.createQueryBuilder('vote') - .select('vote.userId') - .where('vote.noteId = :noteId', { noteId: note.id }) - .innerJoinAndSelect('vote.user', 'user') - .andWhere('user.host IS NULL') - .getMany(); - - const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; - - for (const userId of userIds) { - createNotification(userId, 'pollEnded', { - noteId: note.id, - }); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts deleted file mode 100644 index 198dde6050..0000000000 --- a/packages/backend/src/queue/processors/inbox.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { URL } from 'node:url'; -import Bull from 'bull'; -import httpSignature from '@peertube/http-signature'; -import perform from '@/remote/activitypub/perform.js'; -import Logger from '@/services/logger.js'; -import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { Instances } from '@/models/index.js'; -import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { toPuny, extractDbHost } from '@/misc/convert-host.js'; -import { getApId } from '@/remote/activitypub/type.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import { InboxJobData } from '../types.js'; -import DbResolver from '@/remote/activitypub/db-resolver.js'; -import { resolvePerson } from '@/remote/activitypub/models/person.js'; -import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; -import { StatusError } from '@/misc/fetch.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { UserPublickey } from '@/models/entities/user-publickey.js'; - -const logger = new Logger('inbox'); - -// ユーザーのinboxにアクティビティが届いた時の処理 -export default async (job: Bull.Job): Promise => { - const signature = job.data.signature; // HTTP-signature - const activity = job.data.activity; - - //#region Log - const info = Object.assign({}, activity) as any; - delete info['@context']; - logger.debug(JSON.stringify(info, null, 2)); - //#endregion - - const host = toPuny(new URL(signature.keyId).hostname); - - // ブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(host)) { - return `Blocked request: ${host}`; - } - - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith('acct:')) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - - const dbResolver = new DbResolver(); - - // HTTP-Signature keyIdを元にDBから取得 - let authUser: { - user: CacheableRemoteUser; - key: UserPublickey | null; - } | null = await dbResolver.getAuthUserFromKeyId(signature.keyId); - - // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 - if (authUser == null) { - try { - authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor)); - } catch (e) { - // 対象が4xxならスキップ - if (e instanceof StatusError) { - if (e.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`; - } - throw `Error in actor ${activity.actor} - ${e.statusCode || e}`; - } - } - } - - // それでもわからなければ終了 - if (authUser == null) { - return `skip: failed to resolve user`; - } - - // publicKey がなくても終了 - if (authUser.key == null) { - return `skip: failed to resolve user publicKey`; - } - - // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); - - // また、signatureのsignerは、activity.actorと一致する必要がある - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { - // 一致しなくても、でもLD-Signatureがありそうならそっちも見る - if (activity.signature) { - if (activity.signature.type !== 'RsaSignature2017') { - return `skip: unsupported LD-signature type ${activity.signature.type}`; - } - - // activity.signature.creator: https://example.oom/users/user#main-key - // みたいになっててUserを引っ張れば公開キーも入ることを期待する - if (activity.signature.creator) { - const candicate = activity.signature.creator.replace(/#.*/, ''); - await resolvePerson(candicate).catch(() => null); - } - - // keyIdからLD-Signatureのユーザーを取得 - authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator); - if (authUser == null) { - return `skip: LD-Signatureのユーザーが取得できませんでした`; - } - - if (authUser.key == null) { - return `skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした`; - } - - // LD-Signature検証 - const ldSignature = new LdSignature(); - const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); - if (!verified) { - return `skip: LD-Signatureの検証に失敗しました`; - } - - // もう一度actorチェック - if (authUser.user.uri !== activity.actor) { - return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; - } - - // ブロックしてたら中断 - const ldHost = extractDbHost(authUser.user.uri); - if (meta.blockedHosts.includes(ldHost)) { - return `Blocked request: ${ldHost}`; - } - } else { - return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`; - } - } - - // activity.idがあればホストが署名者のホストであることを確認する - if (typeof activity.id === 'string') { - const signerHost = extractDbHost(authUser.user.uri!); - const activityIdHost = extractDbHost(activity.id); - if (signerHost !== activityIdHost) { - return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; - } - } - - // Update stats - registerOrFetchInstanceDoc(authUser.user.host).then(i => { - Instances.update(i.id, { - latestRequestReceivedAt: new Date(), - lastCommunicatedAt: new Date(), - isNotResponding: false, - }); - - fetchInstanceMetadata(i); - - instanceChart.requestReceived(i.host); - apRequestChart.inbox(); - federationChart.inbox(i.host); - }); - - // アクティビティを処理 - await perform(authUser.user, activity); - return `ok`; -}; diff --git a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts b/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts deleted file mode 100644 index 77da162f6e..0000000000 --- a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import { deleteFileSync } from '@/services/drive/delete-file.js'; -import { DriveFiles } from '@/models/index.js'; -import { MoreThan, Not, IsNull } from 'typeorm'; - -const logger = queueLogger.createSubLogger('clean-remote-files'); - -export default async function cleanRemoteFiles(job: Bull.Job>, done: any): Promise { - logger.info(`Deleting cached remote files...`); - - let deletedCount = 0; - let cursor: any = null; - - while (true) { - const files = await DriveFiles.find({ - where: { - userHost: Not(IsNull()), - isLink: false, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 8, - order: { - id: 1, - }, - }); - - if (files.length === 0) { - job.progress(100); - break; - } - - cursor = files[files.length - 1].id; - - await Promise.all(files.map(file => deleteFileSync(file, true))); - - deletedCount += 8; - - const total = await DriveFiles.countBy({ - userHost: Not(IsNull()), - isLink: false, - }); - - job.progress(deletedCount / total); - } - - logger.succ(`All cahced remote files has been deleted.`); - done(); -} diff --git a/packages/backend/src/queue/processors/object-storage/delete-file.ts b/packages/backend/src/queue/processors/object-storage/delete-file.ts deleted file mode 100644 index c271e3ddd4..0000000000 --- a/packages/backend/src/queue/processors/object-storage/delete-file.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ObjectStorageFileJobData } from '@/queue/types.js'; -import Bull from 'bull'; -import { deleteObjectStorageFile } from '@/services/drive/delete-file.js'; - -export default async (job: Bull.Job) => { - const key: string = job.data.key; - - await deleteObjectStorageFile(key); - - return 'Success'; -}; diff --git a/packages/backend/src/queue/processors/object-storage/index.ts b/packages/backend/src/queue/processors/object-storage/index.ts deleted file mode 100644 index ae6c481fea..0000000000 --- a/packages/backend/src/queue/processors/object-storage/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Bull from 'bull'; -import { ObjectStorageJobData } from '@/queue/types.js'; -import deleteFile from './delete-file.js'; -import cleanRemoteFiles from './clean-remote-files.js'; - -const jobs = { - deleteFile, - cleanRemoteFiles, -} as Record | Bull.ProcessPromiseFunction>; - -export default function(q: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - q.process(k, 16, v); - } -} diff --git a/packages/backend/src/queue/processors/system/check-expired-mutings.ts b/packages/backend/src/queue/processors/system/check-expired-mutings.ts deleted file mode 100644 index 621269e7e1..0000000000 --- a/packages/backend/src/queue/processors/system/check-expired-mutings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Bull from 'bull'; -import { In } from 'typeorm'; -import { Mutings } from '@/models/index.js'; -import { queueLogger } from '../../logger.js'; -import { publishUserEvent } from '@/services/stream.js'; - -const logger = queueLogger.createSubLogger('check-expired-mutings'); - -export async function checkExpiredMutings(job: Bull.Job>, done: any): Promise { - logger.info(`Checking expired mutings...`); - - const expired = await Mutings.createQueryBuilder('muting') - .where('muting.expiresAt IS NOT NULL') - .andWhere('muting.expiresAt < :now', { now: new Date() }) - .innerJoinAndSelect('muting.mutee', 'mutee') - .getMany(); - - if (expired.length > 0) { - await Mutings.delete({ - id: In(expired.map(m => m.id)), - }); - - for (const m of expired) { - publishUserEvent(m.muterId, 'unmute', m.mutee!); - } - } - - logger.succ(`All expired mutings checked.`); - done(); -} diff --git a/packages/backend/src/queue/processors/system/clean-charts.ts b/packages/backend/src/queue/processors/system/clean-charts.ts deleted file mode 100644 index c9169d5acf..0000000000 --- a/packages/backend/src/queue/processors/system/clean-charts.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js'; - -const logger = queueLogger.createSubLogger('clean-charts'); - -export async function cleanCharts(job: Bull.Job>, done: any): Promise { - logger.info(`Clean charts...`); - - await Promise.all([ - federationChart.clean(), - notesChart.clean(), - usersChart.clean(), - activeUsersChart.clean(), - instanceChart.clean(), - perUserNotesChart.clean(), - driveChart.clean(), - perUserReactionsChart.clean(), - hashtagChart.clean(), - perUserFollowingChart.clean(), - perUserDriveChart.clean(), - apRequestChart.clean(), - ]); - - logger.succ(`All charts successfully cleaned.`); - done(); -} diff --git a/packages/backend/src/queue/processors/system/clean.ts b/packages/backend/src/queue/processors/system/clean.ts deleted file mode 100644 index c4f978d7c9..0000000000 --- a/packages/backend/src/queue/processors/system/clean.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Bull from 'bull'; -import { LessThan } from 'typeorm'; -import { UserIps } from '@/models/index.js'; - -import { queueLogger } from '../../logger.js'; - -const logger = queueLogger.createSubLogger('clean'); - -export async function clean(job: Bull.Job>, done: any): Promise { - logger.info('Cleaning...'); - - UserIps.delete({ - createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), - }); - - logger.succ('Cleaned.'); - done(); -} diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts deleted file mode 100644 index 9527d40b0f..0000000000 --- a/packages/backend/src/queue/processors/system/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Bull from 'bull'; -import { tickCharts } from './tick-charts.js'; -import { resyncCharts } from './resync-charts.js'; -import { cleanCharts } from './clean-charts.js'; -import { checkExpiredMutings } from './check-expired-mutings.js'; -import { clean } from './clean.js'; - -const jobs = { - tickCharts, - resyncCharts, - cleanCharts, - checkExpiredMutings, - clean, -} as Record> | Bull.ProcessPromiseFunction>>; - -export default function(dbQueue: Bull.Queue>) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/system/resync-charts.ts b/packages/backend/src/queue/processors/system/resync-charts.ts deleted file mode 100644 index 20012513af..0000000000 --- a/packages/backend/src/queue/processors/system/resync-charts.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import { driveChart, notesChart, usersChart } from '@/services/chart/index.js'; - -const logger = queueLogger.createSubLogger('resync-charts'); - -export async function resyncCharts(job: Bull.Job>, done: any): Promise { - logger.info(`Resync charts...`); - - // TODO: ユーザーごとのチャートも更新する - // TODO: インスタンスごとのチャートも更新する - await Promise.all([ - driveChart.resync(), - notesChart.resync(), - usersChart.resync(), - ]); - - logger.succ(`All charts successfully resynced.`); - done(); -} diff --git a/packages/backend/src/queue/processors/system/tick-charts.ts b/packages/backend/src/queue/processors/system/tick-charts.ts deleted file mode 100644 index 13403f8f73..0000000000 --- a/packages/backend/src/queue/processors/system/tick-charts.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Bull from 'bull'; - -import { queueLogger } from '../../logger.js'; -import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js'; - -const logger = queueLogger.createSubLogger('tick-charts'); - -export async function tickCharts(job: Bull.Job>, done: any): Promise { - logger.info(`Tick charts...`); - - await Promise.all([ - federationChart.tick(false), - notesChart.tick(false), - usersChart.tick(false), - activeUsersChart.tick(false), - instanceChart.tick(false), - perUserNotesChart.tick(false), - driveChart.tick(false), - perUserReactionsChart.tick(false), - hashtagChart.tick(false), - perUserFollowingChart.tick(false), - perUserDriveChart.tick(false), - apRequestChart.tick(false), - ]); - - logger.succ(`All charts successfully ticked.`); - done(); -} diff --git a/packages/backend/src/queue/processors/webhook-deliver.ts b/packages/backend/src/queue/processors/webhook-deliver.ts deleted file mode 100644 index d49206f68f..0000000000 --- a/packages/backend/src/queue/processors/webhook-deliver.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { URL } from 'node:url'; -import Bull from 'bull'; -import Logger from '@/services/logger.js'; -import { WebhookDeliverJobData } from '../types.js'; -import { getResponse, StatusError } from '@/misc/fetch.js'; -import { Webhooks } from '@/models/index.js'; -import config from '@/config/index.js'; - -const logger = new Logger('webhook'); - -export default async (job: Bull.Job) => { - try { - logger.debug(`delivering ${job.data.webhookId}`); - - const res = await getResponse({ - url: job.data.to, - method: 'POST', - headers: { - 'User-Agent': 'Misskey-Hooks', - 'X-Misskey-Host': config.host, - 'X-Misskey-Hook-Id': job.data.webhookId, - 'X-Misskey-Hook-Secret': job.data.secret, - }, - body: JSON.stringify({ - hookId: job.data.webhookId, - userId: job.data.userId, - eventId: job.data.eventId, - createdAt: job.data.createdAt, - type: job.data.type, - body: job.data.content, - }), - }); - - Webhooks.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res.status, - }); - - return 'Success'; - } catch (res) { - Webhooks.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res instanceof StatusError ? res.statusCode : 1, - }); - - if (res instanceof StatusError) { - // 4xx - if (res.isClientError) { - return `${res.statusCode} ${res.statusMessage}`; - } - - // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; - } else { - // DNS error, socket error, timeout ... - throw res; - } - } -}; diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts deleted file mode 100644 index f3a267790c..0000000000 --- a/packages/backend/src/queue/queues.ts +++ /dev/null @@ -1,21 +0,0 @@ -import config from '@/config/index.js'; -import { initialize as initializeQueue } from './initialize.js'; -import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js'; - -export const systemQueue = initializeQueue>('system'); -export const endedPollNotificationQueue = initializeQueue('endedPollNotification'); -export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128); -export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); -export const dbQueue = initializeQueue('db'); -export const objectStorageQueue = initializeQueue('objectStorage'); -export const webhookDeliverQueue = initializeQueue('webhookDeliver', 64); - -export const queues = [ - systemQueue, - endedPollNotificationQueue, - deliverQueue, - inboxQueue, - dbQueue, - objectStorageQueue, - webhookDeliverQueue, -]; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 5ea4725561..18ec997a1b 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,9 +1,9 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note'; -import { User } from '@/models/entities/user.js'; -import { Webhook } from '@/models/entities/webhook'; -import { IActivity } from '@/remote/activitypub/type.js'; -import httpSignature from '@peertube/http-signature'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; +import type { Webhook } from '@/models/entities/Webhook.js'; +import type { IActivity } from '@/core/remote/activitypub/type.js'; +import type httpSignature from '@peertube/http-signature'; export type DeliverJobData = { /** Actor */ diff --git a/packages/backend/src/redis.ts b/packages/backend/src/redis.ts new file mode 100644 index 0000000000..d1678ae65f --- /dev/null +++ b/packages/backend/src/redis.ts @@ -0,0 +1,15 @@ +import Redis from 'ioredis'; +import { loadConfig } from '@/config.js'; + +export function createRedisConnection(): Redis.Redis { + const config = loadConfig(); + + return new Redis({ + port: config.redis.port, + host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + keyPrefix: `${config.redis.prefix}:`, + db: config.redis.db ?? 0, + }); +} diff --git a/packages/backend/src/remote/activitypub/ap-request.ts b/packages/backend/src/remote/activitypub/ap-request.ts deleted file mode 100644 index 8b55f22477..0000000000 --- a/packages/backend/src/remote/activitypub/ap-request.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as crypto from 'node:crypto'; -import { URL } from 'node:url'; - -type Request = { - url: string; - method: string; - headers: Record; -}; - -type PrivateKey = { - privateKeyPem: string; - keyId: string; -}; - -export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }) { - const u = new URL(args.url); - const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; - - const request: Request = { - url: u.href, - method: 'POST', - headers: objectAssignWithLcKey({ - 'Date': new Date().toUTCString(), - 'Host': u.hostname, - 'Content-Type': 'application/activity+json', - 'Digest': digestHeader, - }, args.additionalHeaders), - }; - - const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; -} - -export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }) { - const u = new URL(args.url); - - const request: Request = { - url: u.href, - method: 'GET', - headers: objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json', - 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).hostname, - }, args.additionalHeaders), - }; - - const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; -} - -function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) { - const signingString = genSigningString(request, includeHeaders); - const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); - const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; - - request.headers = objectAssignWithLcKey(request.headers, { - Signature: signatureHeader, - }); - - return { - request, - signingString, - signature, - signatureHeader, - }; -} - -function genSigningString(request: Request, includeHeaders: string[]) { - request.headers = lcObjectKey(request.headers); - - const results: string[] = []; - - for (const key of includeHeaders.map(x => x.toLowerCase())) { - if (key === '(request-target)') { - results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); - } else { - results.push(`${key}: ${request.headers[key]}`); - } - } - - return results.join('\n'); -} - -function lcObjectKey(src: 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; -} - -function objectAssignWithLcKey(a: Record, b: Record) { - return Object.assign(lcObjectKey(a), lcObjectKey(b)); -} diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts deleted file mode 100644 index 846ccf9c00..0000000000 --- a/packages/backend/src/remote/activitypub/audience.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ApObject, getApIds } from './type.js'; -import Resolver from './resolver.js'; -import { resolvePerson } from './models/person.js'; -import { unique, concat } from '@/prelude/array.js'; -import promiseLimit from 'promise-limit'; -import { User, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; - -type Visibility = 'public' | 'home' | 'followers' | 'specified'; - -type AudienceInfo = { - visibility: Visibility, - mentionedUsers: CacheableUser[], - visibleUsers: CacheableUser[], -}; - -export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { - const toGroups = groupingAudience(getApIds(to), actor); - const ccGroups = groupingAudience(getApIds(cc), actor); - - const others = unique(concat([toGroups.other, ccGroups.other])); - - const limit = promiseLimit(2); - const mentionedUsers = (await Promise.all( - others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))) - )).filter((x): x is CacheableUser => x != null); - - if (toGroups.public.length > 0) { - return { - visibility: 'public', - mentionedUsers, - visibleUsers: [], - }; - } - - if (ccGroups.public.length > 0) { - return { - visibility: 'home', - mentionedUsers, - visibleUsers: [], - }; - } - - if (toGroups.followers.length > 0) { - return { - visibility: 'followers', - mentionedUsers, - visibleUsers: [], - }; - } - - return { - visibility: 'specified', - mentionedUsers, - visibleUsers: mentionedUsers, - }; -} - -function groupingAudience(ids: string[], actor: CacheableRemoteUser) { - const groups = { - public: [] as string[], - followers: [] as string[], - other: [] as string[], - }; - - for (const id of ids) { - if (isPublic(id)) { - groups.public.push(id); - } else if (isFollowers(id, actor)) { - groups.followers.push(id); - } else { - groups.other.push(id); - } - } - - groups.other = unique(groups.other); - - return groups; -} - -function isPublic(id: string) { - return [ - 'https://www.w3.org/ns/activitystreams#Public', - 'as#Public', - 'Public', - ].includes(id); -} - -function isFollowers(id: string, actor: CacheableRemoteUser) { - return ( - id === (actor.followersUri || `${actor.uri}/followers`) - ); -} diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts deleted file mode 100644 index 1a02f675ca..0000000000 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ /dev/null @@ -1,155 +0,0 @@ -import escapeRegexp from 'escape-regexp'; -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; -import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; -import { UserPublickey } from '@/models/entities/user-publickey.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; -import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; -import { IObject, getApId } from './type.js'; -import { resolvePerson } from './models/person.js'; - -const publicKeyCache = new Cache(Infinity); -const publicKeyByUserIdCache = new Cache(Infinity); - -export type UriParseResult = { - /** wether the URI was generated by us */ - local: true; - /** id in DB */ - id: string; - /** hint of type, e.g. "notes", "users" */ - type: string; - /** any remaining text after type and id, not including the slash after id. undefined if empty */ - rest?: string; -} | { - /** wether the URI was generated by us */ - local: false; - /** uri in DB */ - uri: string; -}; - -export function parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - // the host part of a URL is case insensitive, so use the 'i' flag. - const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - local: true, - type: matchLocal[1], - id: matchLocal[2], - rest: matchLocal[3], - }; - } else { - return { - local: false, - uri, - }; - } -} - -export default class DbResolver { - constructor() { - } - - /** - * AP Note => Misskey Note in DB - */ - public async getNoteFromApId(value: string | IObject): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== 'notes') return null; - - return await Notes.findOneBy({ - id: parsed.id, - }); - } else { - return await Notes.findOneBy({ - uri: parsed.uri, - }); - } - } - - public async getMessageFromApId(value: string | IObject): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== 'notes') return null; - - return await MessagingMessages.findOneBy({ - id: parsed.id, - }); - } else { - return await MessagingMessages.findOneBy({ - uri: parsed.uri, - }); - } - } - - /** - * AP Person => Misskey User in DB - */ - public async getUserFromApId(value: string | IObject): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== 'users') return null; - - return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ - id: parsed.id, - }).then(x => x ?? undefined)) ?? null; - } else { - return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ - uri: parsed.uri, - })); - } - } - - /** - * AP KeyId => Misskey User and Key - */ - public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: CacheableRemoteUser; - key: UserPublickey; - } | null> { - const key = await publicKeyCache.fetch(keyId, async () => { - const key = await UserPublickeys.findOneBy({ - keyId, - }); - - if (key == null) return null; - - return key; - }, key => key != null); - - if (key == null) return null; - - return { - user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser, - key, - }; - } - - /** - * AP Actor id => Misskey User and Key - */ - public async getAuthUserFromApId(uri: string): Promise<{ - user: CacheableRemoteUser; - key: UserPublickey | null; - } | null> { - const user = await resolvePerson(uri) as CacheableRemoteUser; - - if (user == null) return null; - - const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null); - - return { - user, - key, - }; - } -} diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts deleted file mode 100644 index 4c1999e4cb..0000000000 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Users, Followings } from '@/models/index.js'; -import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js'; -import { deliver } from '@/queue/index.js'; -import { IsNull, Not } from 'typeorm'; - -//#region types -interface IRecipe { - type: string; -} - -interface IFollowersRecipe extends IRecipe { - type: 'Followers'; -} - -interface IDirectRecipe extends IRecipe { - type: 'Direct'; - to: IRemoteUser; -} - -const isFollowers = (recipe: any): recipe is IFollowersRecipe => - recipe.type === 'Followers'; - -const isDirect = (recipe: any): recipe is IDirectRecipe => - recipe.type === 'Direct'; -//#endregion - -export default class DeliverManager { - private actor: { id: User['id']; host: null; }; - private activity: any; - private recipes: IRecipe[] = []; - - /** - * Constructor - * @param actor Actor - * @param activity Activity to deliver - */ - constructor(actor: { id: User['id']; host: null; }, activity: any) { - this.actor = actor; - this.activity = activity; - } - - /** - * Add recipe for followers deliver - */ - public addFollowersRecipe() { - const deliver = { - type: 'Followers', - } as IFollowersRecipe; - - this.addRecipe(deliver); - } - - /** - * Add recipe for direct deliver - * @param to To - */ - public addDirectRecipe(to: IRemoteUser) { - const recipe = { - type: 'Direct', - to, - } as IDirectRecipe; - - this.addRecipe(recipe); - } - - /** - * Add recipe - * @param recipe Recipe - */ - public addRecipe(recipe: IRecipe) { - this.recipes.push(recipe); - } - - /** - * Execute delivers - */ - public async execute() { - if (!Users.isLocalUser(this.actor)) return; - - const inboxes = new Set(); - - /* - build inbox list - - Process follower recipes first to avoid duplication when processing - direct recipes later. - */ - if (this.recipes.some(r => isFollowers(r))) { - // followers deliver - // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう - // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? - const followers = await Followings.find({ - where: { - followeeId: this.actor.id, - followerHost: Not(IsNull()), - }, - select: { - followerSharedInbox: true, - followerInbox: true, - }, - }) as { - followerSharedInbox: string | null; - followerInbox: string; - }[]; - - for (const following of followers) { - const inbox = following.followerSharedInbox || following.followerInbox; - inboxes.add(inbox); - } - } - - this.recipes.filter((recipe): recipe is IDirectRecipe => - // followers recipes have already been processed - isDirect(recipe) - // check that shared inbox has not been added yet - && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) - // check that they actually have an inbox - && recipe.to.inbox != null, - ) - .forEach(recipe => inboxes.add(recipe.to.inbox!)); - - // deliver - for (const inbox of inboxes) { - deliver(this.actor, this.activity, inbox); - } - } -} - -//#region Utilities -/** - * Deliver activity to followers - * @param activity Activity - * @param from Followee - */ -export async function deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { - const manager = new DeliverManager(actor, activity); - manager.addFollowersRecipe(); - await manager.execute(); -} - -/** - * Deliver activity to user - * @param activity Activity - * @param to Target user - */ -export async function deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { - const manager = new DeliverManager(actor, activity); - manager.addDirectRecipe(to); - await manager.execute(); -} -//#endregion diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts deleted file mode 100644 index 4350ef1333..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import accept from '@/services/following/requests/accept.js'; -import { IFollow } from '../../type.js'; -import DbResolver from '../../db-resolver.js'; -import { relayAccepted } from '@/services/relay.js'; - -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - - const dbResolver = new DbResolver(); - const follower = await dbResolver.getUserFromApId(activity.actor); - - if (follower == null) { - return `skip: follower not found`; - } - - if (follower.host != null) { - return `skip: follower is not a local user`; - } - - // relay - const match = activity.id?.match(/follow-relay\/(\w+)/); - if (match) { - return await relayAccepted(match[1]); - } - - await accept(actor, follower); - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts deleted file mode 100644 index 78ef75ade3..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Resolver from '../../resolver.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import acceptFollow from './follow.js'; -import { IAccept, isFollow, getApType } from '../../type.js'; -import { apLogger } from '../../logger.js'; - -const logger = apLogger; - -export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { - const uri = activity.id || activity; - - logger.info(`Accept: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await acceptFollow(actor, object); - - return `skip: Unknown Accept type: ${getApType(object)}`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts deleted file mode 100644 index c813414f93..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IAdd } from '../../type.js'; -import { resolveNote } from '../../models/note.js'; -import { addPinned } from '@/services/i/pin.js'; - -export default async (actor: CacheableRemoteUser, activity: IAdd): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - if (activity.target == null) { - throw new Error('target is null'); - } - - if (activity.target === actor.featured) { - const note = await resolveNote(activity.object); - if (note == null) throw new Error('note not found'); - await addPinned(actor, note.id); - return; - } - - throw new Error(`unknown target: ${activity.target}`); -}; diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts deleted file mode 100644 index ae7e507c99..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Resolver from '../../resolver.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import announceNote from './note.js'; -import { IAnnounce, getApId } from '../../type.js'; -import { apLogger } from '../../logger.js'; - -const logger = apLogger; - -export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { - const uri = getApId(activity); - - logger.info(`Announce: ${uri}`); - - const resolver = new Resolver(); - - const targetUri = getApId(activity.object); - - announceNote(resolver, actor, activity, targetUri); -}; diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts deleted file mode 100644 index 759cb4ae83..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Resolver from '../../resolver.js'; -import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IAnnounce, getApId } from '../../type.js'; -import { fetchNote, resolveNote } from '../../models/note.js'; -import { apLogger } from '../../logger.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { parseAudience } from '../../audience.js'; -import { StatusError } from '@/misc/fetch.js'; -import { Notes } from '@/models/index.js'; - -const logger = apLogger; - -/** - * アナウンスアクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { - const uri = getApId(activity); - - if (actor.isSuspended) { - return; - } - - // アナウンス先をブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(extractDbHost(uri))) return; - - const unlock = await getApLock(uri); - - try { - // 既に同じURIを持つものが登録されていないかチェック - const exist = await fetchNote(uri); - if (exist) { - return; - } - - // Announce対象をresolve - let renote; - try { - renote = await resolveNote(targetUri); - } catch (e) { - // 対象が4xxならスキップ - if (e instanceof StatusError) { - if (e.isClientError) { - logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`); - return; - } - - logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`); - } - throw e; - } - - if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity'; - - logger.info(`Creating the (Re)Note: ${uri}`); - - const activityAudience = await parseAudience(actor, activity.to, activity.cc); - - await post(actor, { - createdAt: activity.published ? new Date(activity.published) : null, - renote, - visibility: activityAudience.visibility, - visibleUsers: activityAudience.visibleUsers, - uri, - }); - } finally { - unlock(); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts deleted file mode 100644 index 5e230ad7b7..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IBlock } from '../../type.js'; -import block from '@/services/blocking/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import DbResolver from '../../db-resolver.js'; -import { Users } from '@/models/index.js'; - -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { - // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず - - const dbResolver = new DbResolver(); - const blockee = await dbResolver.getUserFromApId(activity.object); - - if (blockee == null) { - return `skip: blockee not found`; - } - - if (blockee.host != null) { - return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; - } - - await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id })); - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/create/index.ts b/packages/backend/src/remote/activitypub/kernel/create/index.ts deleted file mode 100644 index c253f9f667..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Resolver from '../../resolver.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import createNote from './note.js'; -import { ICreate, getApId, isPost, getApType } from '../../type.js'; -import { apLogger } from '../../logger.js'; -import { toArray, concat, unique } from '@/prelude/array.js'; - -const logger = apLogger; - -export default async (actor: CacheableRemoteUser, activity: ICreate): Promise => { - const uri = getApId(activity); - - logger.info(`Create: ${uri}`); - - // copy audiences between activity <=> object. - if (typeof activity.object === 'object') { - const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); - const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); - - activity.to = to; - activity.cc = cc; - activity.object.to = to; - activity.object.cc = cc; - } - - // If there is no attributedTo, use Activity actor. - if (typeof activity.object === 'object' && !activity.object.attributedTo) { - activity.object.attributedTo = activity.actor; - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isPost(object)) { - createNote(resolver, actor, object, false, activity); - } else { - logger.warn(`Unknown type: ${getApType(object)}`); - } -}; diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts deleted file mode 100644 index f8dabe06e2..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Resolver from '../../resolver.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { createNote, fetchNote } from '../../models/note.js'; -import { getApId, IObject, ICreate } from '../../type.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { StatusError } from '@/misc/fetch.js'; - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { - const uri = getApId(note); - - if (typeof note === 'object') { - if (actor.uri !== note.attributedTo) { - return `skip: actor.uri !== note.attributedTo`; - } - - if (typeof note.id === 'string') { - if (extractDbHost(actor.uri) !== extractDbHost(note.id)) { - return `skip: host in actor.uri !== note.id`; - } - } - } - - const unlock = await getApLock(uri); - - try { - const exist = await fetchNote(note); - if (exist) return 'skip: note exists'; - - await createNote(note, resolver, silent); - return 'ok'; - } catch (e) { - if (e instanceof StatusError && e.isClientError) { - return `skip ${e.statusCode}`; - } else { - throw e; - } - } finally { - unlock(); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts deleted file mode 100644 index 1f94df033d..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { apLogger } from '../../logger.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; - -const logger = apLogger; - -export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { - logger.info(`Deleting the Actor: ${uri}`); - - if (actor.uri !== uri) { - return `skip: delete actor ${actor.uri} !== ${uri}`; - } - - const user = await Users.findOneByOrFail({ id: actor.id }); - if (user.isDeleted) { - logger.info(`skip: already deleted`); - } - - const job = await createDeleteAccountJob(actor); - - await Users.update(actor.id, { - isDeleted: true, - }); - - return `ok: queued ${job.name} ${job.id}`; -} diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts deleted file mode 100644 index c7064f553b..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import deleteNote from './note.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type.js'; -import { toSingle } from '@/prelude/array.js'; -import { deleteActor } from './actor.js'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: CacheableRemoteUser, activity: IDelete): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - // 削除対象objectのtype - let formerType: string | undefined; - - if (typeof activity.object === 'string') { - // typeが不明だけど、どうせ消えてるのでremote resolveしない - formerType = undefined; - } else { - const object = activity.object as IObject; - if (isTombstone(object)) { - formerType = toSingle(object.formerType); - } else { - formerType = toSingle(object.type); - } - } - - const uri = getApId(activity.object); - - // type不明でもactorとobjectが同じならばそれはPersonに違いない - if (!formerType && actor.uri === uri) { - formerType = 'Person'; - } - - // それでもなかったらおそらくNote - if (!formerType) { - formerType = 'Note'; - } - - if (validPost.includes(formerType)) { - return await deleteNote(actor, uri); - } else if (validActor.includes(formerType)) { - return await deleteActor(actor, uri); - } else { - return `Unknown type ${formerType}`; - } -}; diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts deleted file mode 100644 index 1f44c35562..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import deleteNode from '@/services/note/delete.js'; -import { apLogger } from '../../logger.js'; -import DbResolver from '../../db-resolver.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { deleteMessage } from '@/services/messages/delete.js'; - -const logger = apLogger; - -export default async function(actor: CacheableRemoteUser, uri: string): Promise { - logger.info(`Deleting the Note: ${uri}`); - - const unlock = await getApLock(uri); - - try { - const dbResolver = new DbResolver(); - const note = await dbResolver.getNoteFromApId(uri); - - if (note == null) { - const message = await dbResolver.getMessageFromApId(uri); - if (message == null) return 'message not found'; - - if (message.userId !== actor.id) { - return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; - } - - await deleteMessage(message); - - return 'ok: message deleted'; - } - - if (note.userId !== actor.id) { - return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; - } - - await deleteNode(actor, note); - return 'ok: note deleted'; - } finally { - unlock(); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts deleted file mode 100644 index aa2f1f5362..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import config from '@/config/index.js'; -import { IFlag, getApIds } from '../../type.js'; -import { AbuseUserReports, Users } from '@/models/index.js'; -import { In } from 'typeorm'; -import { genId } from '@/misc/gen-id.js'; - -export default async (actor: CacheableRemoteUser, activity: IFlag): Promise => { - // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので - // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する - const uris = getApIds(activity.object); - - const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); - const users = await Users.findBy({ - id: In(userIds), - }); - if (users.length < 1) return `skip`; - - await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: users[0].id, - targetUserHost: users[0].host, - reporterId: actor.id, - reporterHost: actor.host, - comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, - }); - - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts deleted file mode 100644 index a9e92fa229..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import follow from '@/services/following/create.js'; -import { IFollow } from '../type.js'; -import DbResolver from '../db-resolver.js'; - -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - const dbResolver = new DbResolver(); - const followee = await dbResolver.getUserFromApId(activity.object); - - if (followee == null) { - return `skip: followee not found`; - } - - if (followee.host != null) { - return `skip: フォローしようとしているユーザーはローカルユーザーではありません`; - } - - await follow(actor, followee, activity.id); - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts deleted file mode 100644 index 254a121605..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import create from './create/index.js'; -import performDeleteActivity from './delete/index.js'; -import performUpdateActivity from './update/index.js'; -import { performReadActivity } from './read.js'; -import follow from './follow.js'; -import undo from './undo/index.js'; -import like from './like.js'; -import announce from './announce/index.js'; -import accept from './accept/index.js'; -import reject from './reject/index.js'; -import add from './add/index.js'; -import remove from './remove/index.js'; -import block from './block/index.js'; -import flag from './flag/index.js'; -import { apLogger } from '../logger.js'; -import Resolver from '../resolver.js'; -import { toArray } from '@/prelude/array.js'; -import { Users } from '@/models/index.js'; - -export async function performActivity(actor: CacheableRemoteUser, activity: IObject) { - if (isCollectionOrOrderedCollection(activity)) { - const resolver = new Resolver(); - for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { - const act = await resolver.resolve(item); - try { - await performOneActivity(actor, act); - } catch (err) { - if (err instanceof Error || typeof err === 'string') { - apLogger.error(err); - } - } - } - } else { - await performOneActivity(actor, activity); - } -} - -async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { - if (actor.isSuspended) return; - - if (isCreate(activity)) { - await create(actor, activity); - } else if (isDelete(activity)) { - await performDeleteActivity(actor, activity); - } else if (isUpdate(activity)) { - await performUpdateActivity(actor, activity); - } else if (isRead(activity)) { - await performReadActivity(actor, activity); - } else if (isFollow(activity)) { - await follow(actor, activity); - } else if (isAccept(activity)) { - await accept(actor, activity); - } else if (isReject(activity)) { - await reject(actor, activity); - } else if (isAdd(activity)) { - await add(actor, activity).catch(err => apLogger.error(err)); - } else if (isRemove(activity)) { - await remove(actor, activity).catch(err => apLogger.error(err)); - } else if (isAnnounce(activity)) { - await announce(actor, activity); - } else if (isLike(activity)) { - await like(actor, activity); - } else if (isUndo(activity)) { - await undo(actor, activity); - } else if (isBlock(activity)) { - await block(actor, activity); - } else if (isFlag(activity)) { - await flag(actor, activity); - } else { - apLogger.warn(`unrecognized activity type: ${(activity as any).type}`); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts deleted file mode 100644 index 2b65ff7383..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { ILike, getApId } from '../type.js'; -import create from '@/services/note/reaction/create.js'; -import { fetchNote, extractEmojis } from '../models/note.js'; - -export default async (actor: CacheableRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await extractEmojis(activity.tag || [], actor.host).catch(() => null); - - return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { - return 'skip: already reacted'; - } else { - throw e; - } - }).then(() => 'ok'); -}; diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts deleted file mode 100644 index f7b0bcecdf..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IRead, getApId } from '../type.js'; -import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; -import { MessagingMessages } from '@/models/index.js'; -import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.js'; - -export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise => { - const id = await getApId(activity.object); - - if (!isSelfHost(extractDbHost(id))) { - return `skip: Read to foreign host (${id})`; - } - - const messageId = id.split('/').pop(); - - const message = await MessagingMessages.findOneBy({ id: messageId }); - if (message == null) { - return `skip: message not found`; - } - - if (actor.id !== message.recipientId) { - return `skip: actor is not a message recipient`; - } - - await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]); - return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts deleted file mode 100644 index 824ac69d70..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { remoteReject } from '@/services/following/reject.js'; -import { IFollow } from '../../type.js'; -import DbResolver from '../../db-resolver.js'; -import { relayRejected } from '@/services/relay.js'; -import { Users } from '@/models/index.js'; - -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - - const dbResolver = new DbResolver(); - const follower = await dbResolver.getUserFromApId(activity.actor); - - if (follower == null) { - return `skip: follower not found`; - } - - if (!Users.isLocalUser(follower)) { - return `skip: follower is not a local user`; - } - - // relay - const match = activity.id?.match(/follow-relay\/(\w+)/); - if (match) { - return await relayRejected(match[1]); - } - - await remoteReject(actor, follower); - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts deleted file mode 100644 index 00f08842f4..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Resolver from '../../resolver.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import rejectFollow from './follow.js'; -import { IReject, isFollow, getApType } from '../../type.js'; -import { apLogger } from '../../logger.js'; - -const logger = apLogger; - -export default async (actor: CacheableRemoteUser, activity: IReject): Promise => { - const uri = activity.id || activity; - - logger.info(`Reject: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await rejectFollow(actor, object); - - return `skip: Unknown Reject type: ${getApType(object)}`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts deleted file mode 100644 index 11a994a83b..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IRemove } from '../../type.js'; -import { resolveNote } from '../../models/note.js'; -import { removePinned } from '@/services/i/pin.js'; - -export default async (actor: CacheableRemoteUser, activity: IRemove): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - if (activity.target == null) { - throw new Error('target is null'); - } - - if (activity.target === actor.featured) { - const note = await resolveNote(activity.object); - if (note == null) throw new Error('note not found'); - await removePinned(actor, note.id); - return; - } - - throw new Error(`unknown target: ${activity.target}`); -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts deleted file mode 100644 index a6e3929b0f..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ /dev/null @@ -1,27 +0,0 @@ -import unfollow from '@/services/following/delete.js'; -import cancelRequest from '@/services/following/requests/cancel.js'; -import { IAccept } from '../../type.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Followings } from '@/models/index.js'; -import DbResolver from '../../db-resolver.js'; - -export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { - const dbResolver = new DbResolver(); - - const follower = await dbResolver.getUserFromApId(activity.object); - if (follower == null) { - return `skip: follower not found`; - } - - const following = await Followings.findOneBy({ - followerId: follower.id, - followeeId: actor.id, - }); - - if (following) { - await unfollow(follower, actor); - return `ok: unfollowed`; - } - - return `skip: フォローされていない`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts deleted file mode 100644 index 417f39722f..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Notes } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IAnnounce, getApId } from '../../type.js'; -import deleteNote from '@/services/note/delete.js'; - -export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { - const uri = getApId(activity); - - const note = await Notes.findOneBy({ - uri, - userId: actor.id, - }); - - if (!note) return 'skip: no such Announce'; - - await deleteNote(actor, note); - return 'ok: deleted'; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts deleted file mode 100644 index 4ac6698578..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IBlock } from '../../type.js'; -import unblock from '@/services/blocking/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import DbResolver from '../../db-resolver.js'; -import { Users } from '@/models/index.js'; - -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { - const dbResolver = new DbResolver(); - const blockee = await dbResolver.getUserFromApId(activity.object); - - if (blockee == null) { - return `skip: blockee not found`; - } - - if (blockee.host != null) { - return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; - } - - await unblock(await Users.findOneByOrFail({ id: actor.id }), blockee); - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts deleted file mode 100644 index 6a43c1444a..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ /dev/null @@ -1,41 +0,0 @@ -import unfollow from '@/services/following/delete.js'; -import cancelRequest from '@/services/following/requests/cancel.js'; -import { IFollow } from '../../type.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { FollowRequests, Followings } from '@/models/index.js'; -import DbResolver from '../../db-resolver.js'; - -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - const dbResolver = new DbResolver(); - - const followee = await dbResolver.getUserFromApId(activity.object); - if (followee == null) { - return `skip: followee not found`; - } - - if (followee.host != null) { - return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`; - } - - const req = await FollowRequests.findOneBy({ - followerId: actor.id, - followeeId: followee.id, - }); - - const following = await Followings.findOneBy({ - followerId: actor.id, - followeeId: followee.id, - }); - - if (req) { - await cancelRequest(followee, actor); - return `ok: follow request canceled`; - } - - if (following) { - await unfollow(actor, followee); - return `ok: unfollowed`; - } - - return `skip: リクエストもフォローもされていない`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts deleted file mode 100644 index 27d433eb33..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '../../type.js'; -import unfollow from './follow.js'; -import unblock from './block.js'; -import undoLike from './like.js'; -import undoAccept from './accept.js'; -import { undoAnnounce } from './announce.js'; -import Resolver from '../../resolver.js'; -import { apLogger } from '../../logger.js'; - -const logger = apLogger; - -export default async (actor: CacheableRemoteUser, activity: IUndo): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - logger.info(`Undo: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await unfollow(actor, object); - if (isBlock(object)) return await unblock(actor, object); - if (isLike(object)) return await undoLike(actor, object); - if (isAnnounce(object)) return await undoAnnounce(actor, object); - if (isAccept(object)) return await undoAccept(actor, object); - - return `skip: unknown object type ${getApType(object)}`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts deleted file mode 100644 index 01aeba1fb7..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { ILike, getApId } from '../../type.js'; -import deleteReaction from '@/services/note/reaction/delete.js'; -import { fetchNote } from '../../models/note.js'; - -/** - * Process Undo.Like activity - */ -export default async (actor: CacheableRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await deleteReaction(actor, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return; - throw e; - }); - - return `ok`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts deleted file mode 100644 index 9e8a81bb39..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { getApType, IUpdate, isActor } from '../../type.js'; -import { apLogger } from '../../logger.js'; -import { updateQuestion } from '../../models/question.js'; -import Resolver from '../../resolver.js'; -import { updatePerson } from '../../models/person.js'; - -/** - * Updateアクティビティを捌きます - */ -export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - return `skip: invalid actor`; - } - - apLogger.debug('Update'); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isActor(object)) { - await updatePerson(actor.uri!, resolver, object); - return `ok: Person updated`; - } else if (getApType(object) === 'Question') { - await updateQuestion(object).catch(e => console.log(e)); - return `ok: Question updated`; - } else { - return `skip: Unknown type: ${getApType(object)}`; - } -}; diff --git a/packages/backend/src/remote/activitypub/logger.ts b/packages/backend/src/remote/activitypub/logger.ts deleted file mode 100644 index cab51b3bf5..0000000000 --- a/packages/backend/src/remote/activitypub/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { remoteLogger } from '../logger.js'; - -export const apLogger = remoteLogger.createSubLogger('ap', 'magenta'); diff --git a/packages/backend/src/remote/activitypub/misc/contexts.ts b/packages/backend/src/remote/activitypub/misc/contexts.ts deleted file mode 100644 index aee0d3629c..0000000000 --- a/packages/backend/src/remote/activitypub/misc/contexts.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* eslint:disable:quotemark indent */ -const id_v1 = { - '@context': { - 'id': '@id', - 'type': '@type', - - 'cred': 'https://w3id.org/credentials#', - 'dc': 'http://purl.org/dc/terms/', - 'identity': 'https://w3id.org/identity#', - 'perm': 'https://w3id.org/permissions#', - 'ps': 'https://w3id.org/payswarm#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', - 'sec': 'https://w3id.org/security#', - 'schema': 'http://schema.org/', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - - 'Group': 'https://www.w3.org/ns/activitystreams#Group', - - 'claim': { '@id': 'cred:claim', '@type': '@id' }, - 'credential': { '@id': 'cred:credential', '@type': '@id' }, - 'issued': { '@id': 'cred:issued', '@type': 'xsd:dateTime' }, - 'issuer': { '@id': 'cred:issuer', '@type': '@id' }, - 'recipient': { '@id': 'cred:recipient', '@type': '@id' }, - 'Credential': 'cred:Credential', - 'CryptographicKeyCredential': 'cred:CryptographicKeyCredential', - - 'about': { '@id': 'schema:about', '@type': '@id' }, - 'address': { '@id': 'schema:address', '@type': '@id' }, - 'addressCountry': 'schema:addressCountry', - 'addressLocality': 'schema:addressLocality', - 'addressRegion': 'schema:addressRegion', - 'comment': 'rdfs:comment', - 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, - 'creator': { '@id': 'dc:creator', '@type': '@id' }, - 'description': 'schema:description', - 'email': 'schema:email', - 'familyName': 'schema:familyName', - 'givenName': 'schema:givenName', - 'image': { '@id': 'schema:image', '@type': '@id' }, - 'label': 'rdfs:label', - 'name': 'schema:name', - 'postalCode': 'schema:postalCode', - 'streetAddress': 'schema:streetAddress', - 'title': 'dc:title', - 'url': { '@id': 'schema:url', '@type': '@id' }, - 'Person': 'schema:Person', - 'PostalAddress': 'schema:PostalAddress', - 'Organization': 'schema:Organization', - - 'identityService': { '@id': 'identity:identityService', '@type': '@id' }, - 'idp': { '@id': 'identity:idp', '@type': '@id' }, - 'Identity': 'identity:Identity', - - 'paymentProcessor': 'ps:processor', - 'preferences': { '@id': 'ps:preferences', '@type': '@vocab' }, - - 'cipherAlgorithm': 'sec:cipherAlgorithm', - 'cipherData': 'sec:cipherData', - 'cipherKey': 'sec:cipherKey', - 'digestAlgorithm': 'sec:digestAlgorithm', - 'digestValue': 'sec:digestValue', - 'domain': 'sec:domain', - 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'initializationVector': 'sec:initializationVector', - 'member': { '@id': 'schema:member', '@type': '@id' }, - 'memberOf': { '@id': 'schema:memberOf', '@type': '@id' }, - 'nonce': 'sec:nonce', - 'normalizationAlgorithm': 'sec:normalizationAlgorithm', - 'owner': { '@id': 'sec:owner', '@type': '@id' }, - 'password': 'sec:password', - 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, - 'privateKeyPem': 'sec:privateKeyPem', - 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, - 'publicKeyPem': 'sec:publicKeyPem', - 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, - 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, - 'signature': 'sec:signature', - 'signatureAlgorithm': 'sec:signatureAlgorithm', - 'signatureValue': 'sec:signatureValue', - 'CryptographicKey': 'sec:Key', - 'EncryptedMessage': 'sec:EncryptedMessage', - 'GraphSignature2012': 'sec:GraphSignature2012', - 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', - - 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, - 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, - }, -}; - -const security_v1 = { - '@context': { - 'id': '@id', - 'type': '@type', - - 'dc': 'http://purl.org/dc/terms/', - 'sec': 'https://w3id.org/security#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - - 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016', - 'Ed25519Signature2018': 'sec:Ed25519Signature2018', - 'EncryptedMessage': 'sec:EncryptedMessage', - 'GraphSignature2012': 'sec:GraphSignature2012', - 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', - 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016', - 'CryptographicKey': 'sec:Key', - - 'authenticationTag': 'sec:authenticationTag', - 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm', - 'cipherAlgorithm': 'sec:cipherAlgorithm', - 'cipherData': 'sec:cipherData', - 'cipherKey': 'sec:cipherKey', - 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, - 'creator': { '@id': 'dc:creator', '@type': '@id' }, - 'digestAlgorithm': 'sec:digestAlgorithm', - 'digestValue': 'sec:digestValue', - 'domain': 'sec:domain', - 'encryptionKey': 'sec:encryptionKey', - 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'initializationVector': 'sec:initializationVector', - 'iterationCount': 'sec:iterationCount', - 'nonce': 'sec:nonce', - 'normalizationAlgorithm': 'sec:normalizationAlgorithm', - 'owner': { '@id': 'sec:owner', '@type': '@id' }, - 'password': 'sec:password', - 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, - 'privateKeyPem': 'sec:privateKeyPem', - 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, - 'publicKeyBase58': 'sec:publicKeyBase58', - 'publicKeyPem': 'sec:publicKeyPem', - 'publicKeyWif': 'sec:publicKeyWif', - 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, - 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, - 'salt': 'sec:salt', - 'signature': 'sec:signature', - 'signatureAlgorithm': 'sec:signingAlgorithm', - 'signatureValue': 'sec:signatureValue', - }, -}; - -const activitystreams = { - '@context': { - '@vocab': '_:', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - 'as': 'https://www.w3.org/ns/activitystreams#', - 'ldp': 'http://www.w3.org/ns/ldp#', - 'vcard': 'http://www.w3.org/2006/vcard/ns#', - 'id': '@id', - 'type': '@type', - 'Accept': 'as:Accept', - 'Activity': 'as:Activity', - 'IntransitiveActivity': 'as:IntransitiveActivity', - 'Add': 'as:Add', - 'Announce': 'as:Announce', - 'Application': 'as:Application', - 'Arrive': 'as:Arrive', - 'Article': 'as:Article', - 'Audio': 'as:Audio', - 'Block': 'as:Block', - 'Collection': 'as:Collection', - 'CollectionPage': 'as:CollectionPage', - 'Relationship': 'as:Relationship', - 'Create': 'as:Create', - 'Delete': 'as:Delete', - 'Dislike': 'as:Dislike', - 'Document': 'as:Document', - 'Event': 'as:Event', - 'Follow': 'as:Follow', - 'Flag': 'as:Flag', - 'Group': 'as:Group', - 'Ignore': 'as:Ignore', - 'Image': 'as:Image', - 'Invite': 'as:Invite', - 'Join': 'as:Join', - 'Leave': 'as:Leave', - 'Like': 'as:Like', - 'Link': 'as:Link', - 'Mention': 'as:Mention', - 'Note': 'as:Note', - 'Object': 'as:Object', - 'Offer': 'as:Offer', - 'OrderedCollection': 'as:OrderedCollection', - 'OrderedCollectionPage': 'as:OrderedCollectionPage', - 'Organization': 'as:Organization', - 'Page': 'as:Page', - 'Person': 'as:Person', - 'Place': 'as:Place', - 'Profile': 'as:Profile', - 'Question': 'as:Question', - 'Reject': 'as:Reject', - 'Remove': 'as:Remove', - 'Service': 'as:Service', - 'TentativeAccept': 'as:TentativeAccept', - 'TentativeReject': 'as:TentativeReject', - 'Tombstone': 'as:Tombstone', - 'Undo': 'as:Undo', - 'Update': 'as:Update', - 'Video': 'as:Video', - 'View': 'as:View', - 'Listen': 'as:Listen', - 'Read': 'as:Read', - 'Move': 'as:Move', - 'Travel': 'as:Travel', - 'IsFollowing': 'as:IsFollowing', - 'IsFollowedBy': 'as:IsFollowedBy', - 'IsContact': 'as:IsContact', - 'IsMember': 'as:IsMember', - 'subject': { - '@id': 'as:subject', - '@type': '@id', - }, - 'relationship': { - '@id': 'as:relationship', - '@type': '@id', - }, - 'actor': { - '@id': 'as:actor', - '@type': '@id', - }, - 'attributedTo': { - '@id': 'as:attributedTo', - '@type': '@id', - }, - 'attachment': { - '@id': 'as:attachment', - '@type': '@id', - }, - 'bcc': { - '@id': 'as:bcc', - '@type': '@id', - }, - 'bto': { - '@id': 'as:bto', - '@type': '@id', - }, - 'cc': { - '@id': 'as:cc', - '@type': '@id', - }, - 'context': { - '@id': 'as:context', - '@type': '@id', - }, - 'current': { - '@id': 'as:current', - '@type': '@id', - }, - 'first': { - '@id': 'as:first', - '@type': '@id', - }, - 'generator': { - '@id': 'as:generator', - '@type': '@id', - }, - 'icon': { - '@id': 'as:icon', - '@type': '@id', - }, - 'image': { - '@id': 'as:image', - '@type': '@id', - }, - 'inReplyTo': { - '@id': 'as:inReplyTo', - '@type': '@id', - }, - 'items': { - '@id': 'as:items', - '@type': '@id', - }, - 'instrument': { - '@id': 'as:instrument', - '@type': '@id', - }, - 'orderedItems': { - '@id': 'as:items', - '@type': '@id', - '@container': '@list', - }, - 'last': { - '@id': 'as:last', - '@type': '@id', - }, - 'location': { - '@id': 'as:location', - '@type': '@id', - }, - 'next': { - '@id': 'as:next', - '@type': '@id', - }, - 'object': { - '@id': 'as:object', - '@type': '@id', - }, - 'oneOf': { - '@id': 'as:oneOf', - '@type': '@id', - }, - 'anyOf': { - '@id': 'as:anyOf', - '@type': '@id', - }, - 'closed': { - '@id': 'as:closed', - '@type': 'xsd:dateTime', - }, - 'origin': { - '@id': 'as:origin', - '@type': '@id', - }, - 'accuracy': { - '@id': 'as:accuracy', - '@type': 'xsd:float', - }, - 'prev': { - '@id': 'as:prev', - '@type': '@id', - }, - 'preview': { - '@id': 'as:preview', - '@type': '@id', - }, - 'replies': { - '@id': 'as:replies', - '@type': '@id', - }, - 'result': { - '@id': 'as:result', - '@type': '@id', - }, - 'audience': { - '@id': 'as:audience', - '@type': '@id', - }, - 'partOf': { - '@id': 'as:partOf', - '@type': '@id', - }, - 'tag': { - '@id': 'as:tag', - '@type': '@id', - }, - 'target': { - '@id': 'as:target', - '@type': '@id', - }, - 'to': { - '@id': 'as:to', - '@type': '@id', - }, - 'url': { - '@id': 'as:url', - '@type': '@id', - }, - 'altitude': { - '@id': 'as:altitude', - '@type': 'xsd:float', - }, - 'content': 'as:content', - 'contentMap': { - '@id': 'as:content', - '@container': '@language', - }, - 'name': 'as:name', - 'nameMap': { - '@id': 'as:name', - '@container': '@language', - }, - 'duration': { - '@id': 'as:duration', - '@type': 'xsd:duration', - }, - 'endTime': { - '@id': 'as:endTime', - '@type': 'xsd:dateTime', - }, - 'height': { - '@id': 'as:height', - '@type': 'xsd:nonNegativeInteger', - }, - 'href': { - '@id': 'as:href', - '@type': '@id', - }, - 'hreflang': 'as:hreflang', - 'latitude': { - '@id': 'as:latitude', - '@type': 'xsd:float', - }, - 'longitude': { - '@id': 'as:longitude', - '@type': 'xsd:float', - }, - 'mediaType': 'as:mediaType', - 'published': { - '@id': 'as:published', - '@type': 'xsd:dateTime', - }, - 'radius': { - '@id': 'as:radius', - '@type': 'xsd:float', - }, - 'rel': 'as:rel', - 'startIndex': { - '@id': 'as:startIndex', - '@type': 'xsd:nonNegativeInteger', - }, - 'startTime': { - '@id': 'as:startTime', - '@type': 'xsd:dateTime', - }, - 'summary': 'as:summary', - 'summaryMap': { - '@id': 'as:summary', - '@container': '@language', - }, - 'totalItems': { - '@id': 'as:totalItems', - '@type': 'xsd:nonNegativeInteger', - }, - 'units': 'as:units', - 'updated': { - '@id': 'as:updated', - '@type': 'xsd:dateTime', - }, - 'width': { - '@id': 'as:width', - '@type': 'xsd:nonNegativeInteger', - }, - 'describes': { - '@id': 'as:describes', - '@type': '@id', - }, - 'formerType': { - '@id': 'as:formerType', - '@type': '@id', - }, - 'deleted': { - '@id': 'as:deleted', - '@type': 'xsd:dateTime', - }, - 'inbox': { - '@id': 'ldp:inbox', - '@type': '@id', - }, - 'outbox': { - '@id': 'as:outbox', - '@type': '@id', - }, - 'following': { - '@id': 'as:following', - '@type': '@id', - }, - 'followers': { - '@id': 'as:followers', - '@type': '@id', - }, - 'streams': { - '@id': 'as:streams', - '@type': '@id', - }, - 'preferredUsername': 'as:preferredUsername', - 'endpoints': { - '@id': 'as:endpoints', - '@type': '@id', - }, - 'uploadMedia': { - '@id': 'as:uploadMedia', - '@type': '@id', - }, - 'proxyUrl': { - '@id': 'as:proxyUrl', - '@type': '@id', - }, - 'liked': { - '@id': 'as:liked', - '@type': '@id', - }, - 'oauthAuthorizationEndpoint': { - '@id': 'as:oauthAuthorizationEndpoint', - '@type': '@id', - }, - 'oauthTokenEndpoint': { - '@id': 'as:oauthTokenEndpoint', - '@type': '@id', - }, - 'provideClientKey': { - '@id': 'as:provideClientKey', - '@type': '@id', - }, - 'signClientKey': { - '@id': 'as:signClientKey', - '@type': '@id', - }, - 'sharedInbox': { - '@id': 'as:sharedInbox', - '@type': '@id', - }, - 'Public': { - '@id': 'as:Public', - '@type': '@id', - }, - 'source': 'as:source', - 'likes': { - '@id': 'as:likes', - '@type': '@id', - }, - 'shares': { - '@id': 'as:shares', - '@type': '@id', - }, - 'alsoKnownAs': { - '@id': 'as:alsoKnownAs', - '@type': '@id', - }, - }, -}; - -export const CONTEXTS: Record = { - 'https://w3id.org/identity/v1': id_v1, - 'https://w3id.org/security/v1': security_v1, - 'https://www.w3.org/ns/activitystreams': activitystreams, -}; diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts deleted file mode 100644 index 389039ebed..0000000000 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as mfm from 'mfm-js'; -import { Note } from '@/models/entities/note.js'; -import { toHtml } from '../../../mfm/to-html.js'; - -export default function(note: Note) { - if (!note.text) return ''; - return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); -} diff --git a/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts b/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts deleted file mode 100644 index bb1ba7925c..0000000000 --- a/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IObject } from '../type.js'; -import { extractApHashtagObjects } from '../models/tag.js'; -import { fromHtml } from '../../../mfm/from-html.js'; - -export function htmlToMfm(html: string, tag?: IObject | IObject[]) { - const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); - - return fromHtml(html, hashtagNames); -} diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts deleted file mode 100644 index 362a543ece..0000000000 --- a/packages/backend/src/remote/activitypub/misc/ld-signature.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as crypto from 'node:crypto'; -import jsonld from 'jsonld'; -import { CONTEXTS } from './contexts.js'; -import fetch from 'node-fetch'; -import { httpAgent, httpsAgent } from '@/misc/fetch.js'; - -// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 - -export class LdSignature { - public debug = false; - public preLoad = true; - public loderTimeout = 10 * 1000; - - constructor() { - } - - public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { - const options = { - type: 'RsaSignature2017', - creator, - domain, - nonce: crypto.randomBytes(16).toString('hex'), - created: (created || new Date()).toISOString(), - } as { - type: string; - creator: string; - domain?: string; - nonce: string; - created: string; - }; - - if (!domain) { - delete options.domain; - } - - const toBeSigned = await this.createVerifyData(data, options); - - const signer = crypto.createSign('sha256'); - signer.update(toBeSigned); - signer.end(); - - const signature = signer.sign(privateKey); - - return { - ...data, - signature: { - ...options, - signatureValue: signature.toString('base64'), - }, - }; - } - - public async verifyRsaSignature2017(data: any, publicKey: string): Promise { - const toBeSigned = await this.createVerifyData(data, data.signature); - const verifier = crypto.createVerify('sha256'); - verifier.update(toBeSigned); - return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); - } - - public async createVerifyData(data: any, options: any) { - const transformedOptions = { - ...options, - '@context': 'https://w3id.org/identity/v1', - }; - delete transformedOptions['type']; - delete transformedOptions['id']; - delete transformedOptions['signatureValue']; - const canonizedOptions = await this.normalize(transformedOptions); - const optionsHash = this.sha256(canonizedOptions); - const transformedData = { ...data }; - delete transformedData['signature']; - const cannonidedData = await this.normalize(transformedData); - if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); - const documentHash = this.sha256(cannonidedData); - const verifyData = `${optionsHash}${documentHash}`; - return verifyData; - } - - public async normalize(data: any) { - const customLoader = this.getLoader(); - return await jsonld.normalize(data, { - documentLoader: customLoader, - }); - } - - private getLoader() { - return async (url: string): Promise => { - if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; - - if (this.preLoad) { - if (url in CONTEXTS) { - if (this.debug) console.debug(`HIT: ${url}`); - return { - contextUrl: null, - document: CONTEXTS[url], - documentUrl: url, - }; - } - } - - if (this.debug) console.debug(`MISS: ${url}`); - const document = await this.fetchDocument(url); - return { - contextUrl: null, - document: document, - documentUrl: url, - }; - }; - } - - private async fetchDocument(url: string) { - const json = await fetch(url, { - headers: { - Accept: 'application/ld+json, application/json', - }, - // TODO - //timeout: this.loderTimeout, - agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent, - }).then(res => { - if (!res.ok) { - throw `${res.status} ${res.statusText}`; - } else { - return res.json(); - } - }); - - return json; - } - - public sha256(data: string): string { - const hash = crypto.createHash('sha256'); - hash.update(data); - return hash.digest('hex'); - } -} diff --git a/packages/backend/src/remote/activitypub/models/icon.ts b/packages/backend/src/remote/activitypub/models/icon.ts deleted file mode 100644 index 50794a937d..0000000000 --- a/packages/backend/src/remote/activitypub/models/icon.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type IIcon = { - type: string; - mediaType?: string; - url?: string; -}; diff --git a/packages/backend/src/remote/activitypub/models/identifier.ts b/packages/backend/src/remote/activitypub/models/identifier.ts deleted file mode 100644 index f6c3bb8c88..0000000000 --- a/packages/backend/src/remote/activitypub/models/identifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type IIdentifier = { - type: string; - name: string; - value: string; -}; diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts deleted file mode 100644 index 102b7b1346..0000000000 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; -import Resolver from '../resolver.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { apLogger } from '../logger.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles, Users } from '@/models/index.js'; -import { truncate } from '@/misc/truncate.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; - -const logger = apLogger; - -/** - * Imageを作成します。 - */ -export async function createImage(actor: CacheableRemoteUser, value: any): Promise { - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new Error('actor has been suspended'); - } - - const image = await new Resolver().resolve(value) as any; - - if (image.url == null) { - throw new Error('invalid image: url not privided'); - } - - logger.info(`Creating the Image: ${image.url}`); - - const instance = await fetchMeta(); - - let file = await uploadFromUrl({ - url: image.url, - user: actor, - uri: image.url, - sensitive: image.sensitive, - isLink: !instance.cacheRemoteFiles, - comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH) - }); - - if (file.isLink) { - // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 - // URLを更新する - if (file.url !== image.url) { - await DriveFiles.update({ id: file.id }, { - url: image.url, - uri: image.url, - }); - - file = await DriveFiles.findOneByOrFail({ id: file.id }); - } - } - - return file; -} - -/** - * Imageを解決します。 - * - * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ - * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 - */ -export async function resolveImage(actor: CacheableRemoteUser, value: any): Promise { - // TODO - - // リモートサーバーからフェッチしてきて登録 - return await createImage(actor, value); -} diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts deleted file mode 100644 index 13f77424ec..0000000000 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ /dev/null @@ -1,24 +0,0 @@ -import promiseLimit from 'promise-limit'; -import { toArray, unique } from '@/prelude/array.js'; -import { CacheableUser, User } from '@/models/entities/user.js'; -import { IObject, isMention, IApMention } from '../type.js'; -import Resolver from '../resolver.js'; -import { resolvePerson } from './person.js'; - -export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { - const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); - - const resolver = new Resolver(); - - const limit = promiseLimit(2); - const mentionedUsers = (await Promise.all( - hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); - - return mentionedUsers; -} - -export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { - if (tags == null) return []; - return toArray(tags).filter(isMention); -} diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts deleted file mode 100644 index 5d63f2605a..0000000000 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ /dev/null @@ -1,359 +0,0 @@ -import promiseLimit from 'promise-limit'; - -import config from '@/config/index.js'; -import Resolver from '../resolver.js'; -import post from '@/services/note/create.js'; -import { resolvePerson } from './person.js'; -import { resolveImage } from './image.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { htmlToMfm } from '../misc/html-to-mfm.js'; -import { extractApHashtags } from './tag.js'; -import { unique, toArray, toSingle } from '@/prelude/array.js'; -import { extractPollFromQuestion } from './question.js'; -import vote from '@/services/note/polls/vote.js'; -import { apLogger } from '../logger.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; -import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { genId } from '@/misc/gen-id.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { createMessage } from '@/services/messages/create.js'; -import { parseAudience } from '../audience.js'; -import { extractApMentions } from './mention.js'; -import DbResolver from '../db-resolver.js'; -import { StatusError } from '@/misc/fetch.js'; - -const logger = apLogger; - -export function validateNote(object: any, uri: string) { - const expectHost = extractDbHost(uri); - - if (object == null) { - return new Error('invalid Note: object is null'); - } - - if (!validPost.includes(getApType(object))) { - return new Error(`invalid Note: invalid object type ${getApType(object)}`); - } - - if (object.id && extractDbHost(object.id) !== expectHost) { - return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`); - } - - if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) { - return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`); - } - - return null; -} - -/** - * Noteをフェッチします。 - * - * Misskeyに対象のNoteが登録されていればそれを返します。 - */ -export async function fetchNote(object: string | IObject): Promise { - const dbResolver = new DbResolver(); - return await dbResolver.getNoteFromApId(object); -} - -/** - * Noteを作成します。 - */ -export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { - if (resolver == null) resolver = new Resolver(); - - const object: any = await resolver.resolve(value); - - const entryUri = getApId(value); - const err = validateNote(object, entryUri); - if (err) { - logger.error(`${err.message}`, { - resolver: { - history: resolver.getHistory(), - }, - value: value, - object: object, - }); - throw new Error('invalid note'); - } - - const note: IPost = object; - - logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - - logger.info(`Creating the Note: ${note.id}`); - - // 投稿者をフェッチ - const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; - - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new Error('actor has been suspended'); - } - - const noteAudience = await parseAudience(actor, note.to, note.cc); - let visibility = noteAudience.visibility; - const visibleUsers = noteAudience.visibleUsers; - - // Audience (to, cc) が指定されてなかった場合 - if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している - // こちらから匿名GET出来たものならばpublic - visibility = 'public'; - } - } - - let isTalk = note._misskey_talk && visibility === 'specified'; - - const apMentions = await extractApMentions(note.tag); - const apHashtags = await extractApHashtags(note.tag); - - // 添付ファイル - // TODO: attachmentは必ずしもImageではない - // TODO: attachmentは必ずしも配列ではない - // Noteがsensitiveなら添付もsensitiveにする - const limit = promiseLimit(2); - - note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; - const files = note.attachment - .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise))) - .filter(image => image != null) - : []; - - // リプライ - const reply: Note | null = note.inReplyTo - ? await resolveNote(note.inReplyTo, resolver).then(x => { - if (x == null) { - logger.warn(`Specified inReplyTo, but nout found`); - throw new Error('inReplyTo not found'); - } else { - return x; - } - }).catch(async e => { - // トークだったらinReplyToのエラーは無視 - const uri = getApId(note.inReplyTo); - if (uri.startsWith(config.url + '/')) { - const id = uri.split('/').pop(); - const talk = await MessagingMessages.findOneBy({ id }); - if (talk) { - isTalk = true; - return null; - } - } - - logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`); - throw e; - }) - : null; - - // 引用 - let quote: Note | undefined | null; - - if (note._misskey_quote || note.quoteUrl) { - const tryResolveNote = async (uri: string): Promise<{ - status: 'ok'; - res: Note | null; - } | { - status: 'permerror' | 'temperror'; - }> => { - if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' }; - try { - const res = await resolveNote(uri); - if (res) { - return { - status: 'ok', - res, - }; - } else { - return { - status: 'permerror', - }; - } - } catch (e) { - return { - status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', - }; - } - }; - - const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); - const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); - - quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); - if (!quote) { - if (results.some(x => x.status === 'temperror')) { - throw 'quote resolve failed'; - } - } - } - - const cw = note.summary === '' ? null : note.summary; - - // テキストのパース - let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') { - text = note.source.content; - } else if (typeof note._misskey_content !== 'undefined') { - text = note._misskey_content; - } else if (typeof note.content === 'string') { - text = htmlToMfm(note.content, note.tag); - } - - // vote - if (reply && reply.hasPoll) { - const poll = await Polls.findOneByOrFail({ noteId: reply.id }); - - const tryCreateVote = async (name: string, index: number): Promise => { - if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { - logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); - } else if (index >= 0) { - logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); - await vote(actor, reply, index); - - // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(reply.id); - } - return null; - }; - - if (note.name) { - return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); - } - } - - const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => { - logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }); - - const apEmojis = emojis.map(emoji => emoji.name); - - const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined); - - if (isTalk) { - for (const recipient of visibleUsers) { - await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id); - return null; - } - } - - return await post(actor, { - createdAt: note.published ? new Date(note.published) : null, - files, - reply, - renote: quote, - name: note.name, - cw, - text, - localOnly: false, - visibility, - visibleUsers, - apMentions, - apHashtags, - apEmojis, - poll, - uri: note.id, - url: getOneApHrefNullable(note.url), - }, silent); -} - -/** - * Noteを解決します。 - * - * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ - * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 - */ -export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('missing uri'); - - // ブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 }; - - const unlock = await getApLock(uri); - - try { - //#region このサーバーに既に登録されていたらそれを返す - const exist = await fetchNote(uri); - - if (exist) { - return exist; - } - //#endregion - - if (uri.startsWith(config.url)) { - throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); - } - - // リモートサーバーからフェッチしてきて登録 - // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが - // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await createNote(uri, resolver, true); - } finally { - unlock(); - } -} - -export async function extractEmojis(tags: IObject | IObject[], host: string): Promise { - host = toPuny(host); - - if (!tags) return []; - - const eomjiTags = toArray(tags).filter(isEmoji); - - return await Promise.all(eomjiTags.map(async tag => { - const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); - tag.icon = toSingle(tag.icon); - - const exists = await Emojis.findOneBy({ - host, - name, - }); - - if (exists) { - if ((tag.updated != null && exists.updatedAt == null) - || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) - || (tag.icon!.url !== exists.originalUrl) - ) { - await Emojis.update({ - host, - name, - }, { - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - }); - - return await Emojis.findOneBy({ - host, - name, - }) as Emoji; - } - - return exists; - } - - logger.info(`register emoji host=${host}, name=${name}`); - - return await Emojis.insert({ - id: genId(), - host, - name, - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - aliases: [], - } as Partial).then(x => Emojis.findOneByOrFail(x.identifiers[0])); - })); -} diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts deleted file mode 100644 index 6097e3b6ed..0000000000 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { URL } from 'node:url'; -import promiseLimit from 'promise-limit'; - -import config from '@/config/index.js'; -import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { Note } from '@/models/entities/note.js'; -import { updateUsertags } from '@/services/update-hashtag.js'; -import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; -import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { UserNotePining } from '@/models/entities/user-note-pining.js'; -import { genId } from '@/misc/gen-id.js'; -import { instanceChart, usersChart } from '@/services/chart/index.js'; -import { UserPublickey } from '@/models/entities/user-publickey.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { toArray } from '@/prelude/array.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { truncate } from '@/misc/truncate.js'; -import { StatusError } from '@/misc/fetch.js'; -import { uriPersonCache } from '@/services/user-cache.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { db } from '@/db/postgre.js'; -import { apLogger } from '../logger.js'; -import { htmlToMfm } from '../misc/html-to-mfm.js'; -import { fromHtml } from '../../../mfm/from-html.js'; -import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; -import Resolver from '../resolver.js'; -import { extractApHashtags } from './tag.js'; -import { resolveNote, extractEmojis } from './note.js'; -import { resolveImage } from './image.js'; - -const logger = apLogger; - -const nameLength = 128; -const summaryLength = 2048; - -/** - * Validate and convert to actor object - * @param x Fetched object - * @param uri Fetch target URI - */ -function validateActor(x: IObject, uri: string): IActor { - const expectHost = toPuny(new URL(uri).hostname); - - if (x == null) { - throw new Error('invalid Actor: object is null'); - } - - if (!isActor(x)) { - throw new Error(`invalid Actor type '${x.type}'`); - } - - if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new Error('invalid Actor: wrong id'); - } - - if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new Error('invalid Actor: wrong inbox'); - } - - if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new Error('invalid Actor: wrong username'); - } - - // These fields are only informational, and some AP software allows these - // fields to be very long. If they are too long, we cut them off. This way - // we can at least see these users and their activities. - if (x.name) { - if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new Error('invalid Actor: wrong name'); - } - x.name = truncate(x.name, nameLength); - } - if (x.summary) { - if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new Error('invalid Actor: wrong summary'); - } - x.summary = truncate(x.summary, summaryLength); - } - - const idHost = toPuny(new URL(x.id!).hostname); - if (idHost !== expectHost) { - throw new Error('invalid Actor: id has different host'); - } - - if (x.publicKey) { - if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); - } - - const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname); - if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); - } - } - - return x; -} - -/** - * Personをフェッチします。 - * - * Misskeyに対象のPersonが登録されていればそれを返します。 - */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - const cached = uriPersonCache.get(uri); - if (cached) return cached; - - // URIがこのサーバーを指しているならデータベースからフェッチ - if (uri.startsWith(config.url + '/')) { - const id = uri.split('/').pop(); - const u = await Users.findOneBy({ id }); - if (u) uriPersonCache.set(uri, u); - return u; - } - - //#region このサーバーに既に登録されていたらそれを返す - const exist = await Users.findOneBy({ uri }); - - if (exist) { - uriPersonCache.set(uri, exist); - return exist; - } - //#endregion - - return null; -} - -/** - * Personを作成します。 - */ -export async function createPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - if (uri.startsWith(config.url)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); - } - - if (resolver == null) resolver = new Resolver(); - - const object = await resolver.resolve(uri) as any; - - const person = validateActor(object, uri); - - logger.info(`Creating the Person: ${person.id}`); - - const host = toPuny(new URL(object.id).hostname); - - const { fields } = analyzeAttachments(person.attachment || []); - - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); - - const isBot = getApType(object) === 'Service'; - - const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); - - // Create user - let user: IRemoteUser; - try { - // Start transaction - await db.transaction(async transactionalEntityManager => { - user = await transactionalEntityManager.save(new User({ - id: genId(), - avatarId: null, - bannerId: null, - createdAt: new Date(), - lastFetchedAt: new Date(), - name: truncate(person.name, nameLength), - isLocked: !!person.manuallyApprovesFollowers, - isExplorable: !!person.discoverable, - username: person.preferredUsername, - usernameLower: person.preferredUsername!.toLowerCase(), - host, - inbox: person.inbox, - sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured ? getApId(person.featured) : undefined, - uri: person.id, - tags, - isBot, - isCat: (person as any).isCat === true, - showTimelineReplies: false, - })) as IRemoteUser; - - await transactionalEntityManager.save(new UserProfile({ - userId: user.id, - description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - url: getOneApHrefNullable(person.url), - fields, - birthday: bday ? bday[0] : null, - location: person['vcard:Address'] || null, - userHost: host, - })); - - if (person.publicKey) { - await transactionalEntityManager.save(new UserPublickey({ - userId: user.id, - keyId: person.publicKey.id, - keyPem: person.publicKey.publicKeyPem, - })); - } - }); - } catch (e) { - // duplicate key error - if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 - const u = await Users.findOneBy({ - uri: person.id, - }); - - if (u) { - user = u as IRemoteUser; - } else { - throw new Error('already registered'); - } - } else { - logger.error(e instanceof Error ? e : new Error(e as string)); - throw e; - } - } - - // Register host - registerOrFetchInstanceDoc(host).then(i => { - Instances.increment({ id: i.id }, 'usersCount', 1); - instanceChart.newUser(i.host); - fetchInstanceMetadata(i); - }); - - usersChart.update(user!, true); - - // ハッシュタグ更新 - updateUsertags(user!, tags); - - //#region アバターとヘッダー画像をフェッチ - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : resolveImage(user!, img).catch(() => null), - )); - - const avatarId = avatar ? avatar.id : null; - const bannerId = banner ? banner.id : null; - - await Users.update(user!.id, { - avatarId, - bannerId, - }); - - user!.avatarId = avatarId; - user!.bannerId = bannerId; - //#endregion - - //#region カスタム絵文字取得 - const emojis = await extractEmojis(person.tag || [], host).catch(e => { - logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }); - - const emojiNames = emojis.map(emoji => emoji.name); - - await Users.update(user!.id, { - emojis: emojiNames, - }); - //#endregion - - await updateFeatured(user!.id).catch(err => logger.error(err)); - - return user!; -} - -/** - * Personの情報を更新します。 - * Misskeyに対象のPersonが登録されていなければ無視します。 - * @param uri URI of Person - * @param resolver Resolver - * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) - */ -export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) { - return; - } - - //#region このサーバーに既に登録されているか - const exist = await Users.findOneBy({ uri }) as IRemoteUser; - - if (exist == null) { - return; - } - //#endregion - - if (resolver == null) resolver = new Resolver(); - - const object = hint || await resolver.resolve(uri); - - const person = validateActor(object, uri); - - logger.info(`Updating the Person: ${person.id}`); - - // アバターとヘッダー画像をフェッチ - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : resolveImage(exist, img).catch(() => null), - )); - - // カスタム絵文字取得 - const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => { - logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }); - - const emojiNames = emojis.map(emoji => emoji.name); - - const { fields } = analyzeAttachments(person.attachment || []); - - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); - - const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); - - const updates = { - lastFetchedAt: new Date(), - inbox: person.inbox, - sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured, - emojis: emojiNames, - name: truncate(person.name, nameLength), - tags, - isBot: getApType(object) === 'Service', - isCat: (person as any).isCat === true, - isLocked: !!person.manuallyApprovesFollowers, - isExplorable: !!person.discoverable, - } as Partial; - - if (avatar) { - updates.avatarId = avatar.id; - } - - if (banner) { - updates.bannerId = banner.id; - } - - // Update user - await Users.update(exist.id, updates); - - if (person.publicKey) { - await UserPublickeys.update({ userId: exist.id }, { - keyId: person.publicKey.id, - keyPem: person.publicKey.publicKeyPem, - }); - } - - await UserProfiles.update({ userId: exist.id }, { - url: getOneApHrefNullable(person.url), - fields, - description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - birthday: bday ? bday[0] : null, - location: person['vcard:Address'] || null, - }); - - publishInternalEvent('remoteUserUpdated', { id: exist.id }); - - // ハッシュタグ更新 - updateUsertags(exist, tags); - - // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await Followings.update({ - followerId: exist.id, - }, { - followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - }); - - await updateFeatured(exist.id).catch(err => logger.error(err)); -} - -/** - * Personを解決します。 - * - * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ - * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 - */ -export async function resolvePerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - //#region このサーバーに既に登録されていたらそれを返す - const exist = await fetchPerson(uri); - - if (exist) { - return exist; - } - //#endregion - - // リモートサーバーからフェッチしてきて登録 - if (resolver == null) resolver = new Resolver(); - return await createPerson(uri, resolver); -} - -const services: { - [x: string]: (id: string, username: string) => any - } = { - 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), - 'misskey:authentication:github': (id, login) => ({ id, login }), - 'misskey:authentication:discord': (id, name) => $discord(id, name), - }; - -const $discord = (id: string, name: string) => { - if (typeof name !== 'string') { - name = 'unknown#0000'; - } - const [username, discriminator] = name.split('#'); - return { id, username, discriminator }; -}; - -function addService(target: { [x: string]: any }, source: IApPropertyValue) { - const service = services[source.name]; - - if (typeof source.value !== 'string') { - source.value = 'unknown'; - } - - const [id, username] = source.value.split('@'); - - if (service) { - target[source.name.split(':')[2]] = service(id, username); - } -} - -export function analyzeAttachments(attachments: IObject | IObject[] | undefined) { - const fields: { - name: string, - value: string - }[] = []; - const services: { [x: string]: any } = {}; - - if (Array.isArray(attachments)) { - for (const attachment of attachments.filter(isPropertyValue)) { - if (isPropertyValue(attachment.identifier)) { - addService(services, attachment.identifier); - } else { - fields.push({ - name: attachment.name, - value: fromHtml(attachment.value), - }); - } - } - } - - return { fields, services }; -} - -export async function updateFeatured(userId: User['id']) { - const user = await Users.findOneByOrFail({ id: userId }); - if (!Users.isRemoteUser(user)) return; - if (!user.featured) return; - - logger.info(`Updating the featured: ${user.uri}`); - - const resolver = new Resolver(); - - // Resolve to (Ordered)Collection Object - const collection = await resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); - - // Resolve to Object(may be Note) arrays - const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; - const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x))); - - // Resolve and regist Notes - const limit = promiseLimit(2); - const featuredNotes = await Promise.all(items - .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも - .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)))); - - await db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); - - // とりあえずidを別の時間で生成して順番を維持 - let td = 0; - for (const note of featuredNotes.filter(note => note != null)) { - td -= 1000; - transactionalEntityManager.insert(UserNotePining, { - id: genId(new Date(Date.now() + td)), - createdAt: new Date(), - userId: user.id, - noteId: note!.id, - }); - } - }); -} diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts deleted file mode 100644 index f0321fdf2f..0000000000 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ /dev/null @@ -1,83 +0,0 @@ -import config from '@/config/index.js'; -import Resolver from '../resolver.js'; -import { IObject, IQuestion, isQuestion } from '../type.js'; -import { apLogger } from '../logger.js'; -import { Notes, Polls } from '@/models/index.js'; -import { IPoll } from '@/models/entities/poll.js'; - -export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { - if (resolver == null) resolver = new Resolver(); - - const question = await resolver.resolve(source); - - if (!isQuestion(question)) { - throw new Error('invalid type'); - } - - const multiple = !question.oneOf; - const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; - - if (multiple && !question.anyOf) { - throw new Error('invalid question'); - } - - const choices = question[multiple ? 'anyOf' : 'oneOf']! - .map((x, i) => x.name!); - - const votes = question[multiple ? 'anyOf' : 'oneOf']! - .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); - - return { - choices, - votes, - multiple, - expiresAt, - }; -} - -/** - * Update votes of Question - * @param uri URI of AP Question object - * @returns true if updated - */ -export async function updateQuestion(value: any) { - const uri = typeof value === 'string' ? value : value.id; - - // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); - - //#region このサーバーに既に登録されているか - const note = await Notes.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registed'); - - const poll = await Polls.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registed'); - //#endregion - - // resolve new Question object - const resolver = new Resolver(); - const question = await resolver.resolve(value) as IQuestion; - apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - - if (question.type !== 'Question') throw new Error('object is not a Question'); - - const apChoices = question.oneOf || question.anyOf; - - let changed = false; - - for (const choice of poll.choices) { - const oldCount = poll.votes[poll.choices.indexOf(choice)]; - const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; - - if (oldCount !== newCount) { - changed = true; - poll.votes[poll.choices.indexOf(choice)] = newCount; - } - } - - await Polls.update({ noteId: note.id }, { - votes: poll.votes, - }); - - return changed; -} diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts deleted file mode 100644 index 964dabad04..0000000000 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { toArray } from '@/prelude/array.js'; -import { IObject, isHashtag, IApHashtag } from '../type.js'; - -export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { - if (tags == null) return []; - - const hashtags = extractApHashtagObjects(tags); - - return hashtags.map(tag => { - const m = tag.name.match(/^#(.+)/); - return m ? m[1] : null; - }).filter((x): x is string => x != null); -} - -export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { - if (tags == null) return []; - return toArray(tags).filter(isHashtag); -} diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts deleted file mode 100644 index a3c10ba945..0000000000 --- a/packages/backend/src/remote/activitypub/perform.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IObject } from './type.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { performActivity } from './kernel/index.js'; -import { updatePerson } from './models/person.js'; - -export default async (actor: CacheableRemoteUser, activity: IObject): Promise => { - await performActivity(actor, activity); - - // ついでにリモートユーザーの情報が古かったら更新しておく - if (actor.uri) { - if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - setImmediate(() => { - updatePerson(actor.uri!); - }); - } - } -}; diff --git a/packages/backend/src/remote/activitypub/renderer/accept.ts b/packages/backend/src/remote/activitypub/renderer/accept.ts deleted file mode 100644 index cb01f6a91b..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/accept.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; - -export default (object: any, user: { id: User['id']; host: null }) => ({ - type: 'Accept', - actor: `${config.url}/users/${user.id}`, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/add.ts b/packages/backend/src/remote/activitypub/renderer/add.ts deleted file mode 100644 index ec47884291..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/add.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; - -export default (user: ILocalUser, target: any, object: any) => ({ - type: 'Add', - actor: `${config.url}/users/${user.id}`, - target, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/announce.ts b/packages/backend/src/remote/activitypub/renderer/announce.ts deleted file mode 100644 index 2709fea51d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/announce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; - -export default (object: any, note: Note) => { - const attributedTo = `${config.url}/users/${note.userId}`; - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`]; - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public']; - } else { - return null; - } - - return { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: 'Announce', - published: note.createdAt.toISOString(), - to, - cc, - object, - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts deleted file mode 100644 index 802d7280b1..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ /dev/null @@ -1,20 +0,0 @@ -import config from '@/config/index.js'; -import { Blocking } from '@/models/entities/blocking.js'; - -/** - * Renders a block into its ActivityPub representation. - * - * @param block The block to be rendered. The blockee relation must be loaded. - */ -export function renderBlock(block: Blocking) { - if (block.blockee?.uri == null) { - throw new Error('renderBlock: missing blockee uri'); - } - - return { - type: 'Block', - id: `${config.url}/blocks/${block.id}`, - actor: `${config.url}/users/${block.blockerId}`, - object: block.blockee.uri, - }; -} diff --git a/packages/backend/src/remote/activitypub/renderer/create.ts b/packages/backend/src/remote/activitypub/renderer/create.ts deleted file mode 100644 index 281a3cb2af..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/create.ts +++ /dev/null @@ -1,17 +0,0 @@ -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; - -export default (object: any, note: Note) => { - const activity = { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: 'Create', - published: note.createdAt.toISOString(), - object, - } as any; - - if (object.to) activity.to = object.to; - if (object.cc) activity.cc = object.cc; - - return activity; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/delete.ts b/packages/backend/src/remote/activitypub/renderer/delete.ts deleted file mode 100644 index 4edd3a8807..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/delete.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; - -export default (object: any, user: { id: User['id']; host: null }) => ({ - type: 'Delete', - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), -}); diff --git a/packages/backend/src/remote/activitypub/renderer/document.ts b/packages/backend/src/remote/activitypub/renderer/document.ts deleted file mode 100644 index c973de4c4c..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/document.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; - -export default (file: DriveFile) => ({ - type: 'Document', - mediaType: file.type, - url: DriveFiles.getPublicUrl(file), - name: file.comment, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/emoji.ts b/packages/backend/src/remote/activitypub/renderer/emoji.ts deleted file mode 100644 index 0bf15eefd9..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/emoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index.js'; -import { Emoji } from '@/models/entities/emoji.js'; - -export default (emoji: Emoji) => ({ - id: `${config.url}/emojis/${emoji.name}`, - type: 'Emoji', - name: `:${emoji.name}:`, - updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString, - icon: { - type: 'Image', - mediaType: emoji.type || 'image/png', - url: emoji.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため - }, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts deleted file mode 100644 index 58eadddbaa..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index.js'; -import { IObject, IActivity } from '@/remote/activitypub/type.js'; -import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { getInstanceActor } from '@/services/instance-actor.js'; - -// to anonymise reporters, the reporting actor must be a system user -// object has to be a uri or array of uris -export const renderFlag = (user: ILocalUser, object: [string], content: string) => { - return { - type: 'Flag', - actor: `${config.url}/users/${user.id}`, - content, - object, - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts b/packages/backend/src/remote/activitypub/renderer/follow-relay.ts deleted file mode 100644 index 2c9678090f..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index.js'; -import { Relay } from '@/models/entities/relay.js'; -import { ILocalUser } from '@/models/entities/user.js'; - -export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) { - const follow = { - id: `${config.url}/activities/follow-relay/${relay.id}`, - type: 'Follow', - actor: `${config.url}/users/${relayActor.id}`, - object: 'https://www.w3.org/ns/activitystreams#Public', - }; - - return follow; -} diff --git a/packages/backend/src/remote/activitypub/renderer/follow-user.ts b/packages/backend/src/remote/activitypub/renderer/follow-user.ts deleted file mode 100644 index 9a8a16d749..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from '@/config/index.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; - -/** - * Convert (local|remote)(Follower|Followee)ID to URL - * @param id Follower|Followee ID - */ -export default async function renderFollowUser(id: User['id']): Promise { - const user = await Users.findOneByOrFail({ id: id }); - return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; -} diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts deleted file mode 100644 index 00fac18ad5..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; - -export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { - const follow = { - id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, - type: 'Follow', - actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, - object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, - } as any; - - return follow; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/hashtag.ts b/packages/backend/src/remote/activitypub/renderer/hashtag.ts deleted file mode 100644 index a7b441e006..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/hashtag.ts +++ /dev/null @@ -1,7 +0,0 @@ -import config from '@/config/index.js'; - -export default (tag: string) => ({ - type: 'Hashtag', - href: `${config.url}/tags/${encodeURIComponent(tag)}`, - name: `#${tag}`, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/image.ts b/packages/backend/src/remote/activitypub/renderer/image.ts deleted file mode 100644 index c7d5a31a27..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/image.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; - -export default (file: DriveFile) => ({ - type: 'Image', - url: DriveFiles.getPublicUrl(file), - sensitive: file.isSensitive, - name: file.comment, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts deleted file mode 100644 index f100b77ce5..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import config from '@/config/index.js'; -import { v4 as uuid } from 'uuid'; -import { IActivity } from '../type.js'; -import { LdSignature } from '../misc/ld-signature.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import { User } from '@/models/entities/user.js'; - -export const renderActivity = (x: any): IActivity | null => { - if (x == null) return null; - - if (typeof x === 'object' && x.id == null) { - x.id = `${config.url}/${uuid()}`; - } - - return Object.assign({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - // as non-standards - manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - // schema - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value', - // Misskey - misskey: 'https://misskey-hub.net/ns#', - '_misskey_content': 'misskey:_misskey_content', - '_misskey_quote': 'misskey:_misskey_quote', - '_misskey_reaction': 'misskey:_misskey_reaction', - '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_talk': 'misskey:_misskey_talk', - 'isCat': 'misskey:isCat', - // vcard - vcard: 'http://www.w3.org/2006/vcard/ns#', - }, - ], - }, x); -}; - -export const attachLdSignature = async (activity: any, user: { id: User['id']; host: null; }): Promise => { - if (activity == null) return null; - - const keypair = await getUserKeypair(user.id); - - const ldSignature = new LdSignature(); - ldSignature.debug = false; - activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`); - - return activity; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/key.ts b/packages/backend/src/remote/activitypub/renderer/key.ts deleted file mode 100644 index c4f3d464f8..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/key.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { createPublicKey } from 'node:crypto'; - -export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({ - id: `${config.url}/users/${user.id}${postfix || '/publickey'}`, - type: 'Key', - owner: `${config.url}/users/${user.id}`, - publicKeyPem: createPublicKey(key.publicKey).export({ - type: 'spki', - format: 'pem', - }), -}); diff --git a/packages/backend/src/remote/activitypub/renderer/like.ts b/packages/backend/src/remote/activitypub/renderer/like.ts deleted file mode 100644 index 00fb72e8a3..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/like.ts +++ /dev/null @@ -1,31 +0,0 @@ -import config from '@/config/index.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { Note } from '@/models/entities/note.js'; -import { Emojis } from '@/models/index.js'; -import { IsNull } from 'typeorm'; -import renderEmoji from './emoji.js'; - -export const renderLike = async (noteReaction: NoteReaction, note: Note) => { - const reaction = noteReaction.reaction; - - const object = { - type: 'Like', - id: `${config.url}/likes/${noteReaction.id}`, - actor: `${config.url}/users/${noteReaction.userId}`, - object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`, - content: reaction, - _misskey_reaction: reaction, - } as any; - - if (reaction.startsWith(':')) { - const name = reaction.replace(/:/g, ''); - const emoji = await Emojis.findOneBy({ - name, - host: IsNull(), - }); - - if (emoji) object.tag = [ renderEmoji(emoji) ]; - } - - return object; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/mention.ts b/packages/backend/src/remote/activitypub/renderer/mention.ts deleted file mode 100644 index c7e62e8840..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/mention.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { User, ILocalUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; - -export default (mention: User) => ({ - type: 'Mention', - href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`, - name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts deleted file mode 100644 index b3bafaa3ab..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { Poll } from '@/models/entities/poll.js'; -import toHtml from '../misc/get-note-html.js'; -import renderEmoji from './emoji.js'; -import renderMention from './mention.js'; -import renderHashtag from './hashtag.js'; -import renderDocument from './document.js'; - -export default async function renderNote(note: Note, dive = true, isTalk = false): Promise> { - const getPromisedFiles = async (ids: string[]) => { - if (!ids || ids.length === 0) return []; - const items = await DriveFiles.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; - }; - - let inReplyTo; - let inReplyToNote: Note | null; - - if (note.replyId) { - inReplyToNote = await Notes.findOneBy({ id: note.replyId }); - - if (inReplyToNote != null) { - const inReplyToUser = await Users.findOneBy({ id: inReplyToNote.userId }); - - if (inReplyToUser != null) { - if (inReplyToNote.uri) { - inReplyTo = inReplyToNote.uri; - } else { - if (dive) { - inReplyTo = await renderNote(inReplyToNote, false); - } else { - inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; - } - } - } - } - } else { - inReplyTo = null; - } - - let quote; - - if (note.renoteId) { - const renote = await Notes.findOneBy({ id: note.renoteId }); - - if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; - } - } - - const attributedTo = `${config.url}/users/${note.userId}`; - - const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`].concat(mentions); - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); - } else if (note.visibility === 'followers') { - to = [`${attributedTo}/followers`]; - cc = mentions; - } else { - to = mentions; - } - - const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ - id: In(note.mentions), - }) : []; - - const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); - const mentionTags = mentionedUsers.map(u => renderMention(u)); - - const files = await getPromisedFiles(note.fileIds); - - const text = note.text ?? ''; - let poll: Poll | null = null; - - if (note.hasPoll) { - poll = await Polls.findOneBy({ noteId: note.id }); - } - - let apText = text; - - if (quote) { - apText += `\n\nRE: ${quote}`; - } - - const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - - const content = toHtml(Object.assign({}, note, { - text: apText, - })); - - const emojis = await getEmojis(note.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const tag = [ - ...hashtagTags, - ...mentionTags, - ...apemojis, - ]; - - const asPoll = poll ? { - type: 'Question', - content: toHtml(Object.assign({}, note, { - text: text, - })), - [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, - [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ - type: 'Note', - name: text, - replies: { - type: 'Collection', - totalItems: poll!.votes[i], - }, - })), - } : {}; - - const asTalk = isTalk ? { - _misskey_talk: true, - } : {}; - - return { - id: `${config.url}/notes/${note.id}`, - type: 'Note', - attributedTo, - summary, - content, - _misskey_content: text, - source: { - content: text, - mediaType: "text/x.misskeymarkdown", - }, - _misskey_quote: quote, - quoteUrl: quote, - published: note.createdAt.toISOString(), - to, - cc, - inReplyTo, - attachment: files.map(renderDocument), - sensitive: note.cw != null || files.some(file => file.isSensitive), - tag, - ...asPoll, - ...asTalk, - }; -} - -export async function getEmojis(names: string[]): Promise { - if (names == null || names.length === 0) return []; - - const emojis = await Promise.all( - names.map(name => Emojis.findOneBy({ - name, - host: IsNull(), - })), - ); - - return emojis.filter(emoji => emoji != null) as Emoji[]; -} diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts deleted file mode 100644 index c5e25f577b..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Render OrderedCollectionPage - * @param id URL of self - * @param totalItems Number of total items - * @param orderedItems Items - * @param partOf URL of base - * @param prev URL of prev page (optional) - * @param next URL of next page (optional) - */ -export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page = { - id, - partOf, - type: 'OrderedCollectionPage', - totalItems, - orderedItems, - } as any; - - if (prev) page.prev = prev; - if (next) page.next = next; - - return page; -} diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts deleted file mode 100644 index ff9a77be3d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Render OrderedCollection - * @param id URL of self - * @param totalItems Total number of items - * @param first URL of first page (optional) - * @param last URL of last page (optional) - * @param orderedItems attached objects (optional) - */ -export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: Record[]): { - id: string | null; - type: 'OrderedCollection'; - totalItems: any; - first?: string; - last?: string; - orderedItems?: Record[]; -} { - const page: any = { - id, - type: 'OrderedCollection', - totalItems, - }; - - if (first) page.first = first; - if (last) page.last = last; - if (orderedItems) page.orderedItems = orderedItems; - - return page; -} diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts deleted file mode 100644 index cd2fd74d47..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { URL } from 'node:url'; -import * as mfm from 'mfm-js'; -import renderImage from './image.js'; -import renderKey from './key.js'; -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { toHtml } from '../../../mfm/to-html.js'; -import { getEmojis } from './note.js'; -import renderEmoji from './emoji.js'; -import { IIdentifier } from '../models/identifier.js'; -import renderHashtag from './hashtag.js'; -import { DriveFiles, UserProfiles } from '@/models/index.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; - -export async function renderPerson(user: ILocalUser) { - const id = `${config.url}/users/${user.id}`; - const isSystem = !!user.username.match(/\./); - - const [avatar, banner, profile] = await Promise.all([ - user.avatarId ? DriveFiles.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), - user.bannerId ? DriveFiles.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), - UserProfiles.findOneByOrFail({ userId: user.id }), - ]); - - const attachment: { - type: 'PropertyValue', - name: string, - value: string, - identifier?: IIdentifier - }[] = []; - - if (profile.fields) { - for (const field of profile.fields) { - attachment.push({ - type: 'PropertyValue', - name: field.name, - value: (field.value != null && field.value.match(/^https?:/)) - ? `${new URL(field.value).href}` - : field.value, - }); - } - } - - const emojis = await getEmojis(user.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag)); - - const tag = [ - ...apemojis, - ...hashtagTags, - ]; - - const keypair = await getUserKeypair(user.id); - - const person = { - type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', - id, - inbox: `${id}/inbox`, - outbox: `${id}/outbox`, - followers: `${id}/followers`, - following: `${id}/following`, - featured: `${id}/collections/featured`, - sharedInbox: `${config.url}/inbox`, - endpoints: { sharedInbox: `${config.url}/inbox` }, - url: `${config.url}/@${user.username}`, - preferredUsername: user.username, - name: user.name, - summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, - icon: avatar ? renderImage(avatar) : null, - image: banner ? renderImage(banner) : null, - tag, - manuallyApprovesFollowers: user.isLocked, - discoverable: !!user.isExplorable, - publicKey: renderKey(user, keypair, `#main-key`), - isCat: user.isCat, - attachment: attachment.length ? attachment : undefined, - } as any; - - if (profile?.birthday) { - person['vcard:bday'] = profile.birthday; - } - - if (profile?.location) { - person['vcard:Address'] = profile.location; - } - - return person; -} diff --git a/packages/backend/src/remote/activitypub/renderer/question.ts b/packages/backend/src/remote/activitypub/renderer/question.ts deleted file mode 100644 index d4d1b590af..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/question.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Poll } from '@/models/entities/poll.js'; - -export default async function renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { - const question = { - type: 'Question', - id: `${config.url}/questions/${note.id}`, - actor: `${config.url}/users/${user.id}`, - content: note.text || '', - [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ - name: text, - _misskey_votes: poll.votes[i], - replies: { - type: 'Collection', - totalItems: poll.votes[i], - }, - })), - }; - - return question; -} diff --git a/packages/backend/src/remote/activitypub/renderer/read.ts b/packages/backend/src/remote/activitypub/renderer/read.ts deleted file mode 100644 index a30e649f64..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/read.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; - -export const renderReadActivity = (user: { id: User['id'] }, message: MessagingMessage) => ({ - type: 'Read', - actor: `${config.url}/users/${user.id}`, - object: message.uri, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/reject.ts b/packages/backend/src/remote/activitypub/renderer/reject.ts deleted file mode 100644 index ab4cc1646a..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/reject.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; - -export default (object: any, user: { id: User['id'] }) => ({ - type: 'Reject', - actor: `${config.url}/users/${user.id}`, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/remove.ts b/packages/backend/src/remote/activitypub/renderer/remove.ts deleted file mode 100644 index 1be3edc5d5..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/remove.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; - -export default (user: { id: User['id'] }, target: any, object: any) => ({ - type: 'Remove', - actor: `${config.url}/users/${user.id}`, - target, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/tombstone.ts b/packages/backend/src/remote/activitypub/renderer/tombstone.ts deleted file mode 100644 index 313ca74e9d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/tombstone.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default (id: string) => ({ - id, - type: 'Tombstone', -}); diff --git a/packages/backend/src/remote/activitypub/renderer/undo.ts b/packages/backend/src/remote/activitypub/renderer/undo.ts deleted file mode 100644 index 46631df9ea..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/undo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index.js'; -import { ILocalUser, User } from '@/models/entities/user.js'; - -export default (object: any, user: { id: User['id'] }) => { - if (object == null) return null; - const id = typeof object.id === 'string' && object.id.startsWith(config.url) ? `${object.id}/undo` : undefined; - - return { - type: 'Undo', - ...(id ? { id } : {}), - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/update.ts b/packages/backend/src/remote/activitypub/renderer/update.ts deleted file mode 100644 index cf880f03fc..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; - -export default (object: any, user: { id: User['id'] }) => { - const activity = { - id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user.id}`, - type: 'Update', - to: [ 'https://www.w3.org/ns/activitystreams#Public' ], - object, - published: new Date().toISOString(), - } as any; - - return activity; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/vote.ts b/packages/backend/src/remote/activitypub/renderer/vote.ts deleted file mode 100644 index b6eb8e095d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/vote.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; -import { IRemoteUser, User } from '@/models/entities/user.js'; -import { PollVote } from '@/models/entities/poll-vote.js'; -import { Poll } from '@/models/entities/poll.js'; - -export default async function renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise { - return { - id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`, - actor: `${config.url}/users/${user.id}`, - type: 'Create', - to: [pollOwner.uri], - published: new Date().toISOString(), - object: { - id: `${config.url}/users/${user.id}#votes/${vote.id}`, - type: 'Note', - attributedTo: `${config.url}/users/${user.id}`, - to: [pollOwner.uri], - inReplyTo: note.uri, - name: poll.choices[vote.choice], - }, - }; -} diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts deleted file mode 100644 index 5cbfd8c259..0000000000 --- a/packages/backend/src/remote/activitypub/request.ts +++ /dev/null @@ -1,58 +0,0 @@ -import config from '@/config/index.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import { User } from '@/models/entities/user.js'; -import { getResponse } from '../../misc/fetch.js'; -import { createSignedPost, createSignedGet } from './ap-request.js'; - -export default async (user: { id: User['id'] }, url: string, object: any) => { - const body = JSON.stringify(object); - - const keypair = await getUserKeypair(user.id); - - const req = createSignedPost({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key`, - }, - url, - body, - additionalHeaders: { - 'User-Agent': config.userAgent, - }, - }); - - await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - body, - }); -}; - -/** - * Get AP object with http-signature - * @param user http-signature user - * @param url URL to fetch - */ -export async function signedGet(url: string, user: { id: User['id'] }) { - const keypair = await getUserKeypair(user.id); - - const req = createSignedGet({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key`, - }, - url, - additionalHeaders: { - 'User-Agent': config.userAgent, - }, - }); - - const res = await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - }); - - return await res.json(); -} diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts deleted file mode 100644 index 2f9af43c0c..0000000000 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ /dev/null @@ -1,133 +0,0 @@ -import config from '@/config/index.js'; -import { getJson } from '@/misc/fetch.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { getInstanceActor } from '@/services/instance-actor.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; -import { signedGet } from './request.js'; -import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; -import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js'; -import { parseUri } from './db-resolver.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import { renderLike } from '@/remote/activitypub/renderer/like.js'; -import { renderPerson } from '@/remote/activitypub/renderer/person.js'; -import renderQuestion from '@/remote/activitypub/renderer/question.js'; -import renderCreate from '@/remote/activitypub/renderer/create.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; - -export default class Resolver { - private history: Set; - private user?: ILocalUser; - - constructor() { - this.history = new Set(); - } - - public getHistory(): string[] { - return Array.from(this.history); - } - - public async resolveCollection(value: string | IObject): Promise { - const collection = typeof value === 'string' - ? await this.resolve(value) - : value; - - if (isCollectionOrOrderedCollection(collection)) { - return collection; - } else { - throw new Error(`unrecognized collection type: ${collection.type}`); - } - } - - public async resolve(value: string | IObject): Promise { - if (value == null) { - throw new Error('resolvee is null (or undefined)'); - } - - if (typeof value !== 'string') { - return value; - } - - if (value.includes('#')) { - // URLs with fragment parts cannot be resolved correctly because - // the fragment part does not get transmitted over HTTP(S). - // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); - } - - if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); - } - - this.history.add(value); - - const host = extractDbHost(value); - if (isSelfHost(host)) { - return await this.resolveLocal(value); - } - - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(host)) { - throw new Error('Instance is blocked'); - } - - if (config.signToActivityPubGet && !this.user) { - this.user = await getInstanceActor(); - } - - const object = (this.user - ? await signedGet(value, this.user) - : await getJson(value, 'application/activity+json, application/ld+json')) as IObject; - - if (object == null || ( - Array.isArray(object['@context']) ? - !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : - object['@context'] !== 'https://www.w3.org/ns/activitystreams' - )) { - throw new Error('invalid response'); - } - - return object; - } - - private resolveLocal(url: string): Promise { - const parsed = parseUri(url); - if (!parsed.local) throw new Error('resolveLocal: not local'); - - switch (parsed.type) { - case 'notes': - return Notes.findOneByOrFail({ id: parsed.id }) - .then(note => { - if (parsed.rest === 'activity') { - // this refers to the create activity and not the note itself - return renderActivity(renderCreate(renderNote(note))); - } else { - return renderNote(note); - } - }); - case 'users': - return Users.findOneByOrFail({ id: parsed.id }) - .then(user => renderPerson(user as ILocalUser)); - case 'questions': - // Polls are indexed by the note they are attached to. - return Promise.all([ - Notes.findOneByOrFail({ id: parsed.id }), - Polls.findOneByOrFail({ noteId: parsed.id }), - ]) - .then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll)); - case 'likes': - return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null }))); - case 'follows': - // rest should be - if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); - - return Promise.all( - [parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id })) - ) - .then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url))); - default: - throw new Error(`resolveLocal: type ${type} unhandled`); - } - } -} diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts deleted file mode 100644 index de7eb0ed83..0000000000 --- a/packages/backend/src/remote/activitypub/type.ts +++ /dev/null @@ -1,295 +0,0 @@ -export type obj = { [x: string]: any }; -export type ApObject = IObject | string | (IObject | string)[]; - -export interface IObject { - '@context': string | string[] | obj | obj[]; - type: string | string[]; - id?: string; - summary?: string; - published?: string; - cc?: ApObject; - to?: ApObject; - attributedTo: ApObject; - attachment?: any[]; - inReplyTo?: any; - replies?: ICollection; - content?: string; - name?: string; - startTime?: Date; - endTime?: Date; - icon?: any; - image?: any; - url?: ApObject; - href?: string; - tag?: IObject | IObject[]; - sensitive?: boolean; -} - -/** - * Get array of ActivityStreams Objects id - */ -export function getApIds(value: ApObject | undefined): string[] { - if (value == null) return []; - const array = Array.isArray(value) ? value : [value]; - return array.map(x => getApId(x)); -} - -/** - * Get first ActivityStreams Object id - */ -export function getOneApId(value: ApObject): string { - const firstOne = Array.isArray(value) ? value[0] : value; - return getApId(firstOne); -} - -/** - * Get ActivityStreams Object id - */ -export function getApId(value: string | IObject): string { - if (typeof value === 'string') return value; - if (typeof value.id === 'string') return value.id; - throw new Error('cannot detemine id'); -} - -/** - * Get ActivityStreams Object type - */ -export function getApType(value: IObject): string { - if (typeof value.type === 'string') return value.type; - if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - throw new Error('cannot detect type'); -} - -export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { - const firstOne = Array.isArray(value) ? value[0] : value; - return getApHrefNullable(firstOne); -} - -export function getApHrefNullable(value: string | IObject | undefined): string | undefined { - if (typeof value === 'string') return value; - if (typeof value?.href === 'string') return value.href; - return undefined; -} - -export interface IActivity extends IObject { - //type: 'Activity'; - actor: IObject | string; - object: IObject | string; - target?: IObject | string; - /** LD-Signature */ - signature?: { - type: string; - created: Date; - creator: string; - domain?: string; - nonce?: string; - signatureValue: string; - }; -} - -export interface ICollection extends IObject { - type: 'Collection'; - totalItems: number; - items: ApObject; -} - -export interface IOrderedCollection extends IObject { - type: 'OrderedCollection'; - totalItems: number; - orderedItems: ApObject; -} - -export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; - -export const isPost = (object: IObject): object is IPost => - validPost.includes(getApType(object)); - -export interface IPost extends IObject { - type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; - source?: { - content: string; - mediaType: string; - }; - _misskey_quote?: string; - quoteUrl?: string; - _misskey_talk: boolean; -} - -export interface IQuestion extends IObject { - type: 'Note' | 'Question'; - source?: { - content: string; - mediaType: string; - }; - _misskey_quote?: string; - quoteUrl?: string; - oneOf?: IQuestionChoice[]; - anyOf?: IQuestionChoice[]; - endTime?: Date; - closed?: Date; -} - -export const isQuestion = (object: IObject): object is IQuestion => - getApType(object) === 'Note' || getApType(object) === 'Question'; - -interface IQuestionChoice { - name?: string; - replies?: ICollection; - _misskey_votes?: number; -} -export interface ITombstone extends IObject { - type: 'Tombstone'; - formerType?: string; - deleted?: Date; -} - -export const isTombstone = (object: IObject): object is ITombstone => - getApType(object) === 'Tombstone'; - -export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; - -export const isActor = (object: IObject): object is IActor => - validActor.includes(getApType(object)); - -export interface IActor extends IObject { - type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; - name?: string; - preferredUsername?: string; - manuallyApprovesFollowers?: boolean; - discoverable?: boolean; - inbox: string; - sharedInbox?: string; // 後方互換性のため - publicKey?: { - id: string; - publicKeyPem: string; - }; - followers?: string | ICollection | IOrderedCollection; - following?: string | ICollection | IOrderedCollection; - featured?: string | IOrderedCollection; - outbox: string | IOrderedCollection; - endpoints?: { - sharedInbox?: string; - }; - 'vcard:bday'?: string; - 'vcard:Address'?: string; -} - -export const isCollection = (object: IObject): object is ICollection => - getApType(object) === 'Collection'; - -export const isOrderedCollection = (object: IObject): object is IOrderedCollection => - getApType(object) === 'OrderedCollection'; - -export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => - isCollection(object) || isOrderedCollection(object); - -export interface IApPropertyValue extends IObject { - type: 'PropertyValue'; - identifier: IApPropertyValue; - name: string; - value: string; -} - -export const isPropertyValue = (object: IObject): object is IApPropertyValue => - object && - getApType(object) === 'PropertyValue' && - typeof object.name === 'string' && - typeof (object as any).value === 'string'; - -export interface IApMention extends IObject { - type: 'Mention'; - href: string; -} - -export const isMention = (object: IObject): object is IApMention => - getApType(object) === 'Mention' && - typeof object.href === 'string'; - -export interface IApHashtag extends IObject { - type: 'Hashtag'; - name: string; -} - -export const isHashtag = (object: IObject): object is IApHashtag => - getApType(object) === 'Hashtag' && - typeof object.name === 'string'; - -export interface IApEmoji extends IObject { - type: 'Emoji'; - updated: Date; -} - -export const isEmoji = (object: IObject): object is IApEmoji => - getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; - -export interface ICreate extends IActivity { - type: 'Create'; -} - -export interface IDelete extends IActivity { - type: 'Delete'; -} - -export interface IUpdate extends IActivity { - type: 'Update'; -} - -export interface IRead extends IActivity { - type: 'Read'; -} - -export interface IUndo extends IActivity { - type: 'Undo'; -} - -export interface IFollow extends IActivity { - type: 'Follow'; -} - -export interface IAccept extends IActivity { - type: 'Accept'; -} - -export interface IReject extends IActivity { - type: 'Reject'; -} - -export interface IAdd extends IActivity { - type: 'Add'; -} - -export interface IRemove extends IActivity { - type: 'Remove'; -} - -export interface ILike extends IActivity { - type: 'Like' | 'EmojiReaction' | 'EmojiReact'; - _misskey_reaction?: string; -} - -export interface IAnnounce extends IActivity { - type: 'Announce'; -} - -export interface IBlock extends IActivity { - type: 'Block'; -} - -export interface IFlag extends IActivity { - type: 'Flag'; -} - -export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; -export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; -export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; -export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; -export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; -export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; -export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; -export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; -export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; -export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; -export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; -export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; -export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; -export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/src/remote/logger.ts b/packages/backend/src/remote/logger.ts deleted file mode 100644 index 4921f53bd8..0000000000 --- a/packages/backend/src/remote/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger.js'; - -export const remoteLogger = new Logger('remote', 'cyan'); diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts deleted file mode 100644 index 6fc6f2c4d3..0000000000 --- a/packages/backend/src/remote/resolve-user.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { URL } from 'node:url'; -import webFinger from './webfinger.js'; -import config from '@/config/index.js'; -import { createPerson, updatePerson } from './activitypub/models/person.js'; -import { remoteLogger } from './logger.js'; -import chalk from 'chalk'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { IsNull } from 'typeorm'; - -const logger = remoteLogger.createSubLogger('resolve-user'); - -export async function resolveUser(username: string, host: string | null): Promise { - const usernameLower = username.toLowerCase(); - - if (host == null) { - logger.info(`return local user: ${usernameLower}`); - return await Users.findOneBy({ usernameLower, host: IsNull() }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - host = toPuny(host); - - if (config.host === host) { - logger.info(`return local user: ${usernameLower}`); - return await Users.findOneBy({ usernameLower, host: IsNull() }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - const user = await Users.findOneBy({ usernameLower, host }) as IRemoteUser | null; - - const acctLower = `${usernameLower}@${host}`; - - if (user == null) { - const self = await resolveSelf(acctLower); - - logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); - return await createPerson(self.href); - } - - // ユーザー情報が古い場合は、WebFilgerからやりなおして返す - if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await Users.update(user.id, { - lastFetchedAt: new Date(), - }); - - logger.info(`try resync: ${acctLower}`); - const self = await resolveSelf(acctLower); - - if (user.uri !== self.href) { - // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping. - logger.info(`uri missmatch: ${acctLower}`); - logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); - - // validate uri - const uri = new URL(self.href); - if (uri.hostname !== host) { - throw new Error(`Invalid uri`); - } - - await Users.update({ - usernameLower, - host: host, - }, { - uri: self.href, - }); - } else { - logger.info(`uri is fine: ${acctLower}`); - } - - await updatePerson(self.href); - - logger.info(`return resynced remote user: ${acctLower}`); - return await Users.findOneBy({ uri: self.href }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - logger.info(`return existing remote user: ${acctLower}`); - return user; -} - -async function resolveSelf(acctLower: string) { - logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); - const finger = await webFinger(acctLower).catch(e => { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`); - throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`); - }); - const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); - if (!self) { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); - throw new Error('self link not found'); - } - return self; -} diff --git a/packages/backend/src/remote/webfinger.ts b/packages/backend/src/remote/webfinger.ts deleted file mode 100644 index 337df34c2d..0000000000 --- a/packages/backend/src/remote/webfinger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { URL } from 'node:url'; -import { getJson } from '@/misc/fetch.js'; -import { query as urlQuery } from '@/prelude/url.js'; - -type ILink = { - href: string; - rel?: string; -}; - -type IWebFinger = { - links: ILink[]; - subject: string; -}; - -export default async function(query: string): Promise { - const url = genUrl(query); - - return await getJson(url, 'application/jrd+json, application/json') as IWebFinger; -} - -function genUrl(query: string) { - if (query.match(/^https?:\/\//)) { - const u = new URL(query); - return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); - } - - const m = query.match(/^([^@]+)@(.*)/); - if (m) { - const hostname = m[2]; - return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` }); - } - - throw new Error(`Invalid query (${query})`); -} diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts new file mode 100644 index 0000000000..281da5175d --- /dev/null +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -0,0 +1,584 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Router from '@koa/router'; +import json from 'koa-json-body'; +import httpSignature from '@peertube/http-signature'; +import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; +import * as url from '@/misc/prelude/url.js'; +import { Config } from '@/config.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { QueueService } from '@/core/QueueService.js'; +import type { ILocalUser, User } from '@/models/entities/User.js'; +import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; +import type { Following } from '@/models/entities/Following.js'; +import { countIf } from '@/misc/prelude/array.js'; +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 type { FindOptionsWhere } from 'typeorm'; + +const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; +const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; + +@Injectable() +export class ActivityPubServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private utilityService: UtilityService, + private userEntityService: UserEntityService, + private apRendererService: ApRendererService, + private queueService: QueueService, + private userKeypairStoreService: UserKeypairStoreService, + private queryService: QueryService, + ) { + } + + #setResponseType(ctx: Router.RouterContext) { + const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON); + if (accept === LD_JSON) { + ctx.response.type = LD_JSON; + } else { + ctx.response.type = ACTIVITY_JSON; + } + } + + /** + * Pack Create or Announce Activity + * @param note Note + */ + async #packActivity(note: Note): Promise { + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { + const renote = await Notes.findOneByOrFail({ id: note.renoteId }); + return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note); + } + + return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); + } + + #inbox(ctx: Router.RouterContext) { + let signature; + + try { + signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); + } catch (e) { + ctx.status = 401; + return; + } + + this.queueService.inbox(ctx.request.body, signature); + + ctx.status = 202; + } + + async #followers(ctx: Router.RouterContext) { + const userId = ctx.params.user; + + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { + ctx.status = 400; + return; + } + + const page = ctx.request.query.page === 'true'; + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + }); + + if (user == null) { + ctx.status = 404; + return; + } + + //#region Check ff visibility + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + if (profile.ffVisibility === 'private') { + ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=30'); + return; + } else if (profile.ffVisibility === 'followers') { + ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=30'); + return; + } + //#endregion + + const limit = 10; + const partOf = `${this.config.url}/users/${userId}/followers`; + + if (page) { + const query = { + followeeId: user.id, + } as FindOptionsWhere; + + // カーソルが指定されている場合 + if (cursor) { + query.id = LessThan(cursor); + } + + // Get followers + const followings = await this.followingsRepository.find({ + where: query, + take: limit + 1, + order: { id: -1 }, + }); + + // 「次のページ」があるかどうか + const inStock = followings.length === limit + 1; + if (inStock) followings.pop(); + + const renderedFollowers = await Promise.all(followings.map(following => this.apRendererService.renderFollowUser(following.followerId))); + const rendered = this.apRendererService.renderOrderedCollectionPage( + `${partOf}?${url.query({ + page: 'true', + cursor, + })}`, + user.followersCount, renderedFollowers, partOf, + undefined, + inStock ? `${partOf}?${url.query({ + page: 'true', + cursor: followings[followings.length - 1].id, + })}` : undefined, + ); + + ctx.body = this.apRendererService.renderActivity(rendered); + this.#setResponseType(ctx); + } else { + // index page + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); + ctx.body = this.apRendererService.renderActivity(rendered); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } + } + + async #following(ctx: Router.RouterContext) { + const userId = ctx.params.user; + + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { + ctx.status = 400; + return; + } + + const page = ctx.request.query.page === 'true'; + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + }); + + if (user == null) { + ctx.status = 404; + return; + } + + //#region Check ff visibility + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + if (profile.ffVisibility === 'private') { + ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=30'); + return; + } else if (profile.ffVisibility === 'followers') { + ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=30'); + return; + } + //#endregion + + const limit = 10; + const partOf = `${this.config.url}/users/${userId}/following`; + + if (page) { + const query = { + followerId: user.id, + } as FindOptionsWhere; + + // カーソルが指定されている場合 + if (cursor) { + query.id = LessThan(cursor); + } + + // Get followings + const followings = await this.followingsRepository.find({ + where: query, + take: limit + 1, + order: { id: -1 }, + }); + + // 「次のページ」があるかどうか + const inStock = followings.length === limit + 1; + if (inStock) followings.pop(); + + const renderedFollowees = await Promise.all(followings.map(following => this.apRendererService.renderFollowUser(following.followeeId))); + const rendered = this.apRendererService.renderOrderedCollectionPage( + `${partOf}?${url.query({ + page: 'true', + cursor, + })}`, + user.followingCount, renderedFollowees, partOf, + undefined, + inStock ? `${partOf}?${url.query({ + page: 'true', + cursor: followings[followings.length - 1].id, + })}` : undefined, + ); + + ctx.body = this.apRendererService.renderActivity(rendered); + this.#setResponseType(ctx); + } else { + // index page + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); + ctx.body = this.apRendererService.renderActivity(rendered); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } + } + + async #featured(ctx: Router.RouterContext) { + const userId = ctx.params.user; + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + }); + + if (user == null) { + ctx.status = 404; + return; + } + + const pinings = await this.userNotePiningsRepository.find({ + where: { userId: user.id }, + order: { id: 'DESC' }, + }); + + const pinnedNotes = await Promise.all(pinings.map(pining => + this.notesRepository.findOneByOrFail({ id: pining.noteId }))); + + const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note))); + + const rendered = this.apRendererService.renderOrderedCollection( + `${this.config.url}/users/${userId}/collections/featured`, + renderedNotes.length, undefined, undefined, renderedNotes, + ); + + ctx.body = this.apRendererService.renderActivity(rendered); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } + + async #outbox(ctx: Router.RouterContext) { + const userId = ctx.params.user; + + const sinceId = ctx.request.query.since_id; + if (sinceId != null && typeof sinceId !== 'string') { + ctx.status = 400; + return; + } + + const untilId = ctx.request.query.until_id; + if (untilId != null && typeof untilId !== 'string') { + ctx.status = 400; + return; + } + + const page = ctx.request.query.page === 'true'; + + if (countIf(x => x != null, [sinceId, untilId]) > 1) { + ctx.status = 400; + return; + } + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + }); + + if (user == null) { + ctx.status = 404; + return; + } + + const limit = 20; + const partOf = `${this.config.url}/users/${userId}/outbox`; + + if (page) { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.localOnly = FALSE'); + + const notes = await query.take(limit).getMany(); + + if (sinceId) notes.reverse(); + + const activities = await Promise.all(notes.map(note => this.#packActivity(note))); + const rendered = this.apRendererService.renderOrderedCollectionPage( + `${partOf}?${url.query({ + page: 'true', + since_id: sinceId, + until_id: untilId, + })}`, + user.notesCount, activities, partOf, + notes.length ? `${partOf}?${url.query({ + page: 'true', + since_id: notes[0].id, + })}` : undefined, + notes.length ? `${partOf}?${url.query({ + page: 'true', + until_id: notes[notes.length - 1].id, + })}` : undefined, + ); + + ctx.body = this.apRendererService.renderActivity(rendered); + this.#setResponseType(ctx); + } else { + // index page + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, + `${partOf}?page=true`, + `${partOf}?page=true&since_id=000000000000000000000000`, + ); + ctx.body = this.apRendererService.renderActivity(rendered); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } + } + + async #userInfo(ctx: Router.RouterContext, user: User | null) { + if (user == null) { + ctx.status = 404; + return; + } + + ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } + + public createRouter() { + // Init router + const router = new Router(); + + //#region Routing + function isActivityPubReq(ctx: Router.RouterContext) { + ctx.response.vary('Accept'); + const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON); + return typeof accepted === 'string' && !accepted.match(/html/); + } + + // inbox + router.post('/inbox', json(), ctx => this.#inbox(ctx)); + router.post('/users/:user/inbox', json(), ctx => this.#inbox(ctx)); + + // note + router.get('/notes/:note', async (ctx, next) => { + if (!isActivityPubReq(ctx)) return await next(); + + const note = await this.notesRepository.findOneBy({ + id: ctx.params.note, + visibility: In(['public' as const, 'home' as const]), + localOnly: false, + }); + + if (note == null) { + ctx.status = 404; + return; + } + + // リモートだったらリダイレクト + if (note.userHost != null) { + if (note.uri == null || this.utilityService.isSelfHost(note.userHost)) { + ctx.status = 500; + return; + } + ctx.redirect(note.uri); + return; + } + + ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + }); + + // note activity + router.get('/notes/:note/activity', async ctx => { + const note = await this.notesRepository.findOneBy({ + id: ctx.params.note, + userHost: IsNull(), + visibility: In(['public' as const, 'home' as const]), + localOnly: false, + }); + + if (note == null) { + ctx.status = 404; + return; + } + + ctx.body = this.apRendererService.renderActivity(await this.#packActivity(note)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + }); + + // outbox + router.get('/users/:user/outbox', (ctx) => this.#outbox(ctx)); + + // followers + router.get('/users/:user/followers', (ctx) => this.#followers(ctx)); + + // following + router.get('/users/:user/following', (ctx) => this.#following(ctx)); + + // featured + router.get('/users/:user/collections/featured', (ctx) => this.#featured(ctx)); + + // publickey + router.get('/users/:user/publickey', async ctx => { + const userId = ctx.params.user; + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + }); + + if (user == null) { + ctx.status = 404; + return; + } + + const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); + + if (this.userEntityService.isLocalUser(user)) { + ctx.body = this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + } else { + ctx.status = 400; + } + }); + + router.get('/users/:user', async (ctx, next) => { + if (!isActivityPubReq(ctx)) return await next(); + + const userId = ctx.params.user; + + const user = await this.usersRepository.findOneBy({ + id: userId, + host: IsNull(), + isSuspended: false, + }); + + await this.#userInfo(ctx, user); + }); + + router.get('/@:user', async (ctx, next) => { + if (!isActivityPubReq(ctx)) return await next(); + + const user = await this.usersRepository.findOneBy({ + usernameLower: ctx.params.user.toLowerCase(), + host: IsNull(), + isSuspended: false, + }); + + await this.#userInfo(ctx, user); + }); + //#endregion + + // emoji + router.get('/emojis/:emoji', async ctx => { + const emoji = await this.emojisRepository.findOneBy({ + host: IsNull(), + name: ctx.params.emoji, + }); + + if (emoji == null) { + ctx.status = 404; + return; + } + + ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + }); + + // like + router.get('/likes/:like', async ctx => { + const reaction = await this.noteReactionsRepository.findOneBy({ id: ctx.params.like }); + + if (reaction == null) { + ctx.status = 404; + return; + } + + const note = await this.notesRepository.findOneBy({ id: reaction.noteId }); + + if (note == null) { + ctx.status = 404; + return; + } + + ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + }); + + // follow + router.get('/follows/:follower/:followee', async ctx => { + // This may be used before the follow is completed, so we do not + // check if the following exists. + + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneBy({ + id: ctx.params.follower, + host: IsNull(), + }), + this.usersRepository.findOneBy({ + id: ctx.params.followee, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + ctx.status = 404; + return; + } + + ctx.body = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)); + ctx.set('Cache-Control', 'public, max-age=180'); + this.#setResponseType(ctx); + }); + + return router; + } +} diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts new file mode 100644 index 0000000000..0f4246bfd1 --- /dev/null +++ b/packages/backend/src/server/FileServerService.ts @@ -0,0 +1,178 @@ +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { Inject, Injectable } from '@nestjs/common'; +import Koa from 'koa'; +import cors from '@koa/cors'; +import Router from '@koa/router'; +import send from 'koa-send'; +import rename from 'rename'; +import { Config } from '@/config.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { StatusError } from '@/misc/status-error.js'; +import Logger from '@/logger.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import { VideoProcessingService } from '@/core/VideoProcessingService.js'; +import { InternalStorageService } from '@/core/InternalStorageService.js'; +import { contentDisposition } from '@/misc/content-disposition.js'; +import { FileInfoService } from '@/core/FileInfoService.js'; + +const serverLogger = new Logger('server', 'gray', false); + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const assets = `${_dirname}/../../server/file/assets/`; + +const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { + serverLogger.error(e); + ctx.status = 500; + ctx.set('Cache-Control', 'max-age=300'); +}; + +@Injectable() +export class FileServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private fileInfoService: FileInfoService, + private downloadService: DownloadService, + private imageProcessingService: ImageProcessingService, + private videoProcessingService: VideoProcessingService, + private internalStorageService: InternalStorageService, + ) { + } + + public createServer() { + const app = new Koa(); + app.use(cors()); + app.use(async (ctx, next) => { + ctx.set('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); + await next(); + }); + + // Init router + const router = new Router(); + + router.get('/app-default.jpg', ctx => { + const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + ctx.body = file; + ctx.set('Content-Type', 'image/jpeg'); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + }); + + router.get('/:key', ctx => this.#sendDriveFile(ctx)); + router.get('/:key/(.*)', ctx => this.#sendDriveFile(ctx)); + + // Register router + app.use(router.routes()); + + return app; + } + + async #sendDriveFile(ctx: Koa.Context) { + const key = ctx.params.key; + + // Fetch drive file + const file = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.accessKey = :accessKey', { accessKey: key }) + .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key }) + .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key }) + .getOne(); + + if (file == null) { + ctx.status = 404; + ctx.set('Cache-Control', 'max-age=86400'); + await send(ctx as any, '/dummy.png', { root: assets }); + return; + } + + const isThumbnail = file.thumbnailAccessKey === key; + const isWebpublic = file.webpublicAccessKey === key; + + if (!file.storedInternal) { + if (file.isLink && file.uri) { // 期限切れリモートファイル + const [path, cleanup] = await createTemp(); + + try { + await this.downloadService.downloadUrl(file.uri, path); + + const { mime, ext } = await this.fileInfoService.detectType(path); + + const convertFile = async () => { + if (isThumbnail) { + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(mime)) { + return await this.imageProcessingService.convertToWebp(path, 498, 280); + } else if (mime.startsWith('video/')) { + return await this.videoProcessingService.generateVideoThumbnail(path); + } + } + + if (isWebpublic) { + if (['image/svg+xml'].includes(mime)) { + return await this.imageProcessingService.convertToPng(path, 2048, 2048); + } + } + + return { + data: fs.readFileSync(path), + ext, + type: mime, + }; + }; + + const image = await convertFile(); + ctx.body = image.data; + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + } catch (err) { + serverLogger.error(`${err}`); + + if (err instanceof StatusError && err.isClientError) { + ctx.status = err.statusCode; + ctx.set('Cache-Control', 'max-age=86400'); + } else { + ctx.status = 500; + ctx.set('Cache-Control', 'max-age=300'); + } + } finally { + cleanup(); + } + return; + } + + ctx.status = 204; + ctx.set('Cache-Control', 'max-age=86400'); + return; + } + + if (isThumbnail || isWebpublic) { + const { mime, ext } = await this.fileInfoService.detectType(this.internalStorageService.resolvePath(key)); + const filename = rename(file.name, { + suffix: isThumbnail ? '-thumb' : '-web', + extname: ext ? `.${ext}` : undefined, + }).toString(); + + ctx.body = this.internalStorageService.read(key); + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream'); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + ctx.set('Content-Disposition', contentDisposition('inline', filename)); + } else { + const readable = this.internalStorageService.read(file.accessKey!); + readable.on('error', commonReadableHandlerGenerator(ctx)); + ctx.body = readable; + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream'); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + ctx.set('Content-Disposition', contentDisposition('inline', file.name)); + } + } +} + diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts new file mode 100644 index 0000000000..b9cee38f0a --- /dev/null +++ b/packages/backend/src/server/MediaProxyServerService.ts @@ -0,0 +1,137 @@ +import * as fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import Koa from 'koa'; +import cors from '@koa/cors'; +import Router from '@koa/router'; +import sharp from 'sharp'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import type { IImage } from '@/core/ImageProcessingService.js'; +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { StatusError } from '@/misc/status-error.js'; +import Logger from '@/logger.js'; +import { FileInfoService } from '@/core/FileInfoService.js'; + +const serverLogger = new Logger('server', 'gray', false); + +@Injectable() +export class MediaProxyServerService { + constructor( + @Inject(DI.config) + private config: Config, + + private fileInfoService: FileInfoService, + private downloadService: DownloadService, + private imageProcessingService: ImageProcessingService, + ) { + } + + public createServer() { + const app = new Koa(); + app.use(cors()); + app.use(async (ctx, next) => { + ctx.set('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); + await next(); + }); + + // Init router + const router = new Router(); + + router.get('/:url*', ctx => this.#handler(ctx)); + + // Register router + app.use(router.routes()); + + return app; + } + + async #handler(ctx: Koa.Context) { + const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; + + if (typeof url !== 'string') { + ctx.status = 400; + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + try { + await this.downloadService.downloadUrl(url, path); + + const { mime, ext } = await this.fileInfoService.detectType(path); + const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); + + let image: IImage; + + if ('static' in ctx.query && isConvertibleImage) { + image = await this.imageProcessingService.convertToWebp(path, 498, 280); + } else if ('preview' in ctx.query && isConvertibleImage) { + image = await this.imageProcessingService.convertToWebp(path, 200, 200); + } else if ('badge' in ctx.query) { + if (!isConvertibleImage) { + // 画像でないなら404でお茶を濁す + throw new StatusError('Unexpected mime', 404); + } + + const mask = sharp(path) + .resize(96, 96, { + fit: 'inside', + withoutEnlargement: false, + }) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .toColorspace('b-w'); + + const stats = await mask.clone().stats(); + + if (stats.entropy < 0.1) { + // エントロピーがあまりない場合は404にする + throw new StatusError('Skip to provide badge', 404); + } + + const data = sharp({ + create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(await mask.png().toBuffer(), 'eor'); + + image = { + data: await data.png().toBuffer(), + ext: 'png', + type: 'image/png', + }; + } else if (mime === 'image/svg+xml') { + image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, 1); + } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { + throw new StatusError('Rejected type', 403, 'Rejected type'); + } else { + image = { + data: fs.readFileSync(path), + ext, + type: mime, + }; + } + + ctx.set('Content-Type', image.type); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + ctx.body = image.data; + } catch (err) { + serverLogger.error(`${err}`); + + if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) { + ctx.status = err.statusCode; + } else { + ctx.status = 500; + } + } finally { + cleanup(); + } + } +} diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts new file mode 100644 index 0000000000..04a5f1484b --- /dev/null +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -0,0 +1,129 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Router from '@koa/router'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { NotesRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +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'; + +const nodeinfo2_1path = '/nodeinfo/2.1'; +const nodeinfo2_0path = '/nodeinfo/2.0'; + +@Injectable() +export class NodeinfoServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, + private metaService: MetaService, + ) { + } + + public getLinks() { + return [/* (awaiting release) { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: config.url + nodeinfo2_1path + }, */{ + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: this.config.url + nodeinfo2_0path, + }]; + } + + public createRouter() { + const router = new Router(); + + const nodeinfo2 = async () => { + const now = Date.now(); + const [ + meta, + total, + activeHalfyear, + activeMonth, + localPosts, + ] = await Promise.all([ + this.metaService.fetch(true), + this.usersRepository.count({ where: { host: IsNull() } }), + this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }), + this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }), + this.notesRepository.count({ where: { userHost: IsNull() } }), + ]); + + const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; + + return { + software: { + name: 'misskey', + version: this.config.version, + repository: meta.repositoryUrl, + }, + protocols: ['activitypub'], + services: { + inbound: [] as string[], + outbound: ['atom1.0', 'rss2.0'], + }, + openRegistrations: !meta.disableRegistration, + usage: { + users: { total, activeHalfyear, activeMonth }, + localPosts, + localComments: 0, + }, + metadata: { + nodeName: meta.name, + nodeDescription: meta.description, + maintainer: { + name: meta.maintainerName, + email: meta.maintainerEmail, + }, + langs: meta.langs, + tosUrl: meta.ToSUrl, + repositoryUrl: meta.repositoryUrl, + feedbackUrl: meta.feedbackUrl, + disableRegistration: meta.disableRegistration, + disableLocalTimeline: meta.disableLocalTimeline, + disableGlobalTimeline: meta.disableGlobalTimeline, + emailRequiredForSignup: meta.emailRequiredForSignup, + enableHcaptcha: meta.enableHcaptcha, + enableRecaptcha: meta.enableRecaptcha, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + enableTwitterIntegration: meta.enableTwitterIntegration, + enableGithubIntegration: meta.enableGithubIntegration, + enableDiscordIntegration: meta.enableDiscordIntegration, + enableEmail: meta.enableEmail, + enableServiceWorker: meta.enableServiceWorker, + proxyAccountName: proxyAccount ? proxyAccount.username : null, + themeColor: meta.themeColor ?? '#86b300', + }, + }; + }; + + const cache = new Cache>>(1000 * 60 * 10); + + router.get(nodeinfo2_1path, async ctx => { + const base = await cache.fetch(null, () => nodeinfo2()); + + ctx.body = { version: '2.1', ...base }; + ctx.set('Cache-Control', 'public, max-age=600'); + }); + + router.get(nodeinfo2_0path, async ctx => { + const base = await cache.fetch(null, () => nodeinfo2()); + + delete base.software.repository; + + ctx.body = { version: '2.0', ...base }; + ctx.set('Cache-Control', 'public, max-age=600'); + }); + + return router; + } +} diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts new file mode 100644 index 0000000000..f05eda1cb8 --- /dev/null +++ b/packages/backend/src/server/ServerModule.ts @@ -0,0 +1,92 @@ +import { Module } from '@nestjs/common'; +import { EndpointsModule } from '@/server/api/EndpointsModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { ApiCallService } from './api/ApiCallService.js'; +import { FileServerService } from './FileServerService.js'; +import { MediaProxyServerService } from './MediaProxyServerService.js'; +import { NodeinfoServerService } from './NodeinfoServerService.js'; +import { ServerService } from './ServerService.js'; +import { WellKnownServerService } from './WellKnownServerService.js'; +import { GetterService } from './api/common/GetterService.js'; +import { DiscordServerService } from './api/integration/DiscordServerService.js'; +import { GithubServerService } from './api/integration/GithubServerService.js'; +import { TwitterServerService } from './api/integration/TwitterServerService.js'; +import { ChannelsService } from './api/stream/ChannelsService.js'; +import { ActivityPubServerService } from './ActivityPubServerService.js'; +import { ApiLoggerService } from './api/ApiLoggerService.js'; +import { ApiServerService } from './api/ApiServerService.js'; +import { AuthenticateService } from './api/AuthenticateService.js'; +import { RateLimiterService } from './api/RateLimiterService.js'; +import { SigninApiService } from './api/SigninApiService.js'; +import { SigninService } from './api/SigninService.js'; +import { SignupApiService } from './api/SignupApiService.js'; +import { StreamingApiServerService } from './api/StreamingApiServerService.js'; +import { ClientServerService } from './web/ClientServerService.js'; +import { FeedService } from './web/FeedService.js'; +import { UrlPreviewService } from './web/UrlPreviewService.js'; +import { MainChannelService } from './api/stream/channels/main.js'; +import { AdminChannelService } from './api/stream/channels/admin.js'; +import { AntennaChannelService } from './api/stream/channels/antenna.js'; +import { ChannelChannelService } from './api/stream/channels/channel.js'; +import { DriveChannelService } from './api/stream/channels/drive.js'; +import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js'; +import { HashtagChannelService } from './api/stream/channels/hashtag.js'; +import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js'; +import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js'; +import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js'; +import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js'; +import { MessagingChannelService } from './api/stream/channels/messaging.js'; +import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; +import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; +import { UserListChannelService } from './api/stream/channels/user-list.js'; + +@Module({ + imports: [ + EndpointsModule, + CoreModule, + ], + providers: [ + ClientServerService, + FeedService, + UrlPreviewService, + ActivityPubServerService, + FileServerService, + MediaProxyServerService, + NodeinfoServerService, + ServerService, + WellKnownServerService, + GetterService, + DiscordServerService, + GithubServerService, + TwitterServerService, + ChannelsService, + ApiCallService, + ApiLoggerService, + ApiServerService, + AuthenticateService, + RateLimiterService, + SigninApiService, + SigninService, + SignupApiService, + StreamingApiServerService, + MainChannelService, + AdminChannelService, + AntennaChannelService, + ChannelChannelService, + DriveChannelService, + GlobalTimelineChannelService, + HashtagChannelService, + HomeTimelineChannelService, + HybridTimelineChannelService, + LocalTimelineChannelService, + MessagingIndexChannelService, + MessagingChannelService, + QueueStatsChannelService, + ServerStatsChannelService, + UserListChannelService, + ], + exports: [ + ServerService, + ], +}) +export class ServerModule {} diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts new file mode 100644 index 0000000000..2ee7135c2a --- /dev/null +++ b/packages/backend/src/server/ServerService.ts @@ -0,0 +1,177 @@ +import cluster from 'node:cluster'; +import * as fs from 'node:fs'; +import * as http from 'node:http'; +import { Inject, Injectable } from '@nestjs/common'; +import Koa from 'koa'; +import Router from '@koa/router'; +import mount from 'koa-mount'; +import koaLogger from 'koa-logger'; +import * as slow from 'koa-slow'; +import { IsNull } from 'typeorm'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { Config } from '@/config.js'; +import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import Logger from '@/logger.js'; +import { envOption } from '@/env.js'; +import * as Acct from '@/misc/acct.js'; +import { genIdenticon } from '@/misc/gen-identicon.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ActivityPubServerService } from './ActivityPubServerService.js'; +import { NodeinfoServerService } from './NodeinfoServerService.js'; +import { ApiServerService } from './api/ApiServerService.js'; +import { StreamingApiServerService } from './api/StreamingApiServerService.js'; +import { WellKnownServerService } from './WellKnownServerService.js'; +import { MediaProxyServerService } from './MediaProxyServerService.js'; +import { FileServerService } from './FileServerService.js'; +import { ClientServerService } from './web/ClientServerService.js'; + +const serverLogger = new Logger('server', 'gray', false); + +@Injectable() +export class ServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private apiServerService: ApiServerService, + private streamingApiServerService: StreamingApiServerService, + private activityPubServerService: ActivityPubServerService, + private wellKnownServerService: WellKnownServerService, + private nodeinfoServerService: NodeinfoServerService, + private fileServerService: FileServerService, + private mediaProxyServerService: MediaProxyServerService, + private clientServerService: ClientServerService, + private globalEventService: GlobalEventService, + ) { + } + + public launch() { + // Init app + const koa = new Koa(); + koa.proxy = true; + + if (!['production', 'test'].includes(process.env.NODE_ENV ?? '')) { + // Logger + koa.use(koaLogger(str => { + serverLogger.info(str); + })); + + // Delay + if (envOption.slow) { + koa.use(slow({ + delay: 3000, + })); + } + } + + // HSTS + // 6months (15552000sec) + if (this.config.url.startsWith('https') && !this.config.disableHsts) { + koa.use(async (ctx, next) => { + ctx.set('strict-transport-security', 'max-age=15552000; preload'); + await next(); + }); + } + + koa.use(mount('/api', this.apiServerService.createApiServer(koa))); + koa.use(mount('/files', this.fileServerService.createServer())); + koa.use(mount('/proxy', this.mediaProxyServerService.createServer())); + + // Init router + const router = new Router(); + + // Routing + router.use(this.activityPubServerService.createRouter().routes()); + router.use(this.nodeinfoServerService.createRouter().routes()); + router.use(this.wellKnownServerService.createRouter().routes()); + + router.get('/avatar/@:acct', async ctx => { + const { username, host } = Acct.parse(ctx.params.acct); + const user = await this.usersRepository.findOne({ + where: { + usernameLower: username.toLowerCase(), + host: (host == null) || (host === this.config.host) ? IsNull() : host, + isSuspended: false, + }, + relations: ['avatar'], + }); + + if (user) { + ctx.redirect(this.userEntityService.getAvatarUrlSync(user)); + } else { + ctx.redirect('/static-assets/user-unknown.png'); + } + }); + + router.get('/identicon/:x', async ctx => { + const [temp, cleanup] = await createTemp(); + await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); + ctx.set('Content-Type', 'image/png'); + ctx.body = fs.createReadStream(temp).on('close', () => cleanup()); + }); + + router.get('/verify-email/:code', async ctx => { + const profile = await this.userProfilesRepository.findOneBy({ + emailVerifyCode: ctx.params.code, + }); + + if (profile != null) { + ctx.body = 'Verify succeeded!'; + ctx.status = 200; + + await this.userProfilesRepository.update({ userId: profile.userId }, { + emailVerified: true, + emailVerifyCode: null, + }); + + this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { + detail: true, + includeSecrets: true, + })); + } else { + ctx.status = 404; + } + }); + + // Register router + koa.use(router.routes()); + + koa.use(mount(this.clientServerService.createApp())); + + const server = http.createServer(koa.callback()); + + this.streamingApiServerService.attachStreamingApi(server); + + server.on('error', e => { + switch ((e as any).code) { + case 'EACCES': + serverLogger.error(`You do not have permission to listen on port ${this.config.port}.`); + break; + case 'EADDRINUSE': + serverLogger.error(`Port ${this.config.port} is already in use by another process.`); + break; + default: + serverLogger.error(e); + break; + } + + if (cluster.isWorker) { + process.send!('listenFailed'); + } else { + // disableClustering + process.exit(1); + } + }); + + server.listen(this.config.port); + } +} diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts new file mode 100644 index 0000000000..7f827d439b --- /dev/null +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -0,0 +1,168 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Router from '@koa/router'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; +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'; + +@Injectable() +export class WellKnownServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private nodeinfoServerService: NodeinfoServerService, + ) { + } + + public createRouter() { + const router = new Router(); + + const XRD = (...x: { element: string, value?: string, attributes?: Record }[]) => + `${x.map(({ element, value, attributes }) => + `<${ + Object.entries(typeof attributes === 'object' && attributes || {}).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element) + }${ + typeof value === 'string' ? `>${escapeValue(value)}`).reduce((a, c) => a + c, '')}`; + + const allPath = '/.well-known/(.*)'; + const webFingerPath = '/.well-known/webfinger'; + const jrd = 'application/jrd+json'; + const xrd = 'application/xrd+xml'; + + router.use(allPath, async (ctx, next) => { + ctx.set({ + 'Access-Control-Allow-Headers': 'Accept', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Expose-Headers': 'Vary', + }); + await next(); + }); + + router.options(allPath, async ctx => { + ctx.status = 204; + }); + + router.get('/.well-known/host-meta', async ctx => { + ctx.set('Content-Type', xrd); + ctx.body = XRD({ element: 'Link', attributes: { + rel: 'lrdd', + type: xrd, + template: `${this.config.url}${webFingerPath}?resource={uri}`, + } }); + }); + + router.get('/.well-known/host-meta.json', async ctx => { + ctx.set('Content-Type', jrd); + ctx.body = { + links: [{ + rel: 'lrdd', + type: jrd, + template: `${this.config.url}${webFingerPath}?resource={uri}`, + }], + }; + }); + + router.get('/.well-known/nodeinfo', async ctx => { + ctx.body = { links: this.nodeinfoServerService.getLinks() }; + }); + + /* TODO +router.get('/.well-known/change-password', async ctx => { +}); +*/ + + router.get(webFingerPath, async ctx => { + const fromId = (id: User['id']): FindOptionsWhere => ({ + id, + host: IsNull(), + isSuspended: false, + }); + + const generateQuery = (resource: string): FindOptionsWhere | number => + resource.startsWith(`${this.config.url.toLowerCase()}/users/`) ? + fromId(resource.split('/').pop()!) : + fromAcct(Acct.parse( + resource.startsWith(`${this.config.url.toLowerCase()}/@`) ? resource.split('/').pop()! : + resource.startsWith('acct:') ? resource.slice('acct:'.length) : + resource)); + + const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => + !acct.host || acct.host === this.config.host.toLowerCase() ? { + usernameLower: acct.username, + host: IsNull(), + isSuspended: false, + } : 422; + + if (typeof ctx.query.resource !== 'string') { + ctx.status = 400; + return; + } + + const query = generateQuery(ctx.query.resource.toLowerCase()); + + if (typeof query === 'number') { + ctx.status = query; + return; + } + + const user = await this.usersRepository.findOneBy(query); + + if (user == null) { + ctx.status = 404; + return; + } + + const subject = `acct:${user.username}@${this.config.host}`; + const self = { + rel: 'self', + type: 'application/activity+json', + href: `${this.config.url}/users/${user.id}`, + }; + const profilePage = { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${this.config.url}/@${user.username}`, + }; + const subscribe = { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${this.config.url}/authorize-follow?acct={uri}`, + }; + + if (ctx.accepts(jrd, xrd) === xrd) { + ctx.body = XRD( + { element: 'Subject', value: subject }, + { element: 'Link', attributes: self }, + { element: 'Link', attributes: profilePage }, + { element: 'Link', attributes: subscribe }); + ctx.type = xrd; + } else { + ctx.body = { + subject, + links: [self, profilePage, subscribe], + }; + ctx.type = jrd; + } + + ctx.vary('Accept'); + ctx.set('Cache-Control', 'public, max-age=180'); + }); + + // Return 404 for other .well-known + router.all(allPath, async ctx => { + ctx.status = 404; + }); + + return router; + } +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts deleted file mode 100644 index cd5f917c40..0000000000 --- a/packages/backend/src/server/activitypub.ts +++ /dev/null @@ -1,254 +0,0 @@ -import Router from '@koa/router'; -import json from 'koa-json-body'; -import httpSignature from '@peertube/http-signature'; - -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import renderKey from '@/remote/activitypub/renderer/key.js'; -import { renderPerson } from '@/remote/activitypub/renderer/person.js'; -import renderEmoji from '@/remote/activitypub/renderer/emoji.js'; -import Outbox, { packActivity } from './activitypub/outbox.js'; -import Followers from './activitypub/followers.js'; -import Following from './activitypub/following.js'; -import Featured from './activitypub/featured.js'; -import { inbox as processInbox } from '@/queue/index.js'; -import { isSelfHost } from '@/misc/convert-host.js'; -import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; -import { ILocalUser, User } from '@/models/entities/user.js'; -import { In, IsNull, Not } from 'typeorm'; -import { renderLike } from '@/remote/activitypub/renderer/like.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; - -// Init router -const router = new Router(); - -//#region Routing - -function inbox(ctx: Router.RouterContext) { - let signature; - - try { - signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); - } catch (e) { - ctx.status = 401; - return; - } - - processInbox(ctx.request.body, signature); - - ctx.status = 202; -} - -const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; -const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; - -function isActivityPubReq(ctx: Router.RouterContext) { - ctx.response.vary('Accept'); - const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON); - return typeof accepted === 'string' && !accepted.match(/html/); -} - -export function setResponseType(ctx: Router.RouterContext) { - const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON); - if (accept === LD_JSON) { - ctx.response.type = LD_JSON; - } else { - ctx.response.type = ACTIVITY_JSON; - } -} - -// inbox -router.post('/inbox', json(), inbox); -router.post('/users/:user/inbox', json(), inbox); - -// note -router.get('/notes/:note', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(['public' as const, 'home' as const]), - localOnly: false, - }); - - if (note == null) { - ctx.status = 404; - return; - } - - // リモートだったらリダイレクト - if (note.userHost != null) { - if (note.uri == null || isSelfHost(note.userHost)) { - ctx.status = 500; - return; - } - ctx.redirect(note.uri); - return; - } - - ctx.body = renderActivity(await renderNote(note, false)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// note activity -router.get('/notes/:note/activity', async ctx => { - const note = await Notes.findOneBy({ - id: ctx.params.note, - userHost: IsNull(), - visibility: In(['public' as const, 'home' as const]), - localOnly: false, - }); - - if (note == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await packActivity(note)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// outbox -router.get('/users/:user/outbox', Outbox); - -// followers -router.get('/users/:user/followers', Followers); - -// following -router.get('/users/:user/following', Following); - -// featured -router.get('/users/:user/collections/featured', Featured); - -// publickey -router.get('/users/:user/publickey', async ctx => { - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - const keypair = await getUserKeypair(user.id); - - if (Users.isLocalUser(user)) { - ctx.body = renderActivity(renderKey(user, keypair)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } else { - ctx.status = 400; - } -}); - -// user -async function userInfo(ctx: Router.RouterContext, user: User | null) { - if (user == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderPerson(user as ILocalUser)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -} - -router.get('/users/:user', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); - -router.get('/@:user', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const user = await Users.findOneBy({ - usernameLower: ctx.params.user.toLowerCase(), - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); -//#endregion - -// emoji -router.get('/emojis/:emoji', async ctx => { - const emoji = await Emojis.findOneBy({ - host: IsNull(), - name: ctx.params.emoji, - }); - - if (emoji == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderEmoji(emoji)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// like -router.get('/likes/:like', async ctx => { - const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); - - if (reaction == null) { - ctx.status = 404; - return; - } - - const note = await Notes.findOneBy({ id: reaction.noteId }); - - if (note == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderLike(reaction, note)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// follow -router.get('/follows/:follower/:followee', async ctx => { - // This may be used before the follow is completed, so we do not - // check if the following exists. - - const [follower, followee] = await Promise.all([ - Users.findOneBy({ - id: ctx.params.follower, - host: IsNull(), - }), - Users.findOneBy({ - id: ctx.params.followee, - host: Not(IsNull()), - }), - ]); - - if (follower == null || followee == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(renderFollow(follower, followee)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -export default router; diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts deleted file mode 100644 index c03fd1049f..0000000000 --- a/packages/backend/src/server/activitypub/featured.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Router from '@koa/router'; -import config from '@/config/index.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; -import { setResponseType } from '../activitypub.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import { Users, Notes, UserNotePinings } from '@/models/index.js'; -import { IsNull } from 'typeorm'; - -export default async (ctx: Router.RouterContext) => { - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - const pinings = await UserNotePinings.find({ - where: { userId: user.id }, - order: { id: 'DESC' }, - }); - - const pinnedNotes = await Promise.all(pinings.map(pining => - Notes.findOneByOrFail({ id: pining.noteId }))); - - const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note))); - - const rendered = renderOrderedCollection( - `${config.url}/users/${userId}/collections/featured`, - renderedNotes.length, undefined, undefined, renderedNotes, - ); - - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}; diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts deleted file mode 100644 index beb48713a6..0000000000 --- a/packages/backend/src/server/activitypub/followers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Router from '@koa/router'; -import { FindOptionsWhere, IsNull, LessThan } from 'typeorm'; -import config from '@/config/index.js'; -import * as url from '@/prelude/url.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; -import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; -import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { Following } from '@/models/entities/following.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - const userId = ctx.params.user; - - const cursor = ctx.request.query.cursor; - if (cursor != null && typeof cursor !== 'string') { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === 'true'; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } - //#endregion - - const limit = 10; - const partOf = `${config.url}/users/${userId}/followers`; - - if (page) { - const query = { - followeeId: user.id, - } as FindOptionsWhere; - - // カーソルが指定されている場合 - if (cursor) { - query.id = LessThan(cursor); - } - - // Get followers - const followings = await Followings.find({ - where: query, - take: limit + 1, - order: { id: -1 }, - }); - - // 「次のページ」があるかどうか - const inStock = followings.length === limit + 1; - if (inStock) followings.pop(); - - const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId))); - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - cursor, - })}`, - user.followersCount, renderedFollowers, partOf, - undefined, - inStock ? `${partOf}?${url.query({ - page: 'true', - cursor: followings[followings.length - 1].id, - })}` : undefined, - ); - - ctx.body = renderActivity(rendered); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts deleted file mode 100644 index 3a25a6316c..0000000000 --- a/packages/backend/src/server/activitypub/following.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Router from '@koa/router'; -import { LessThan, IsNull, FindOptionsWhere } from 'typeorm'; -import config from '@/config/index.js'; -import * as url from '@/prelude/url.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; -import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; -import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { Following } from '@/models/entities/following.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - const userId = ctx.params.user; - - const cursor = ctx.request.query.cursor; - if (cursor != null && typeof cursor !== 'string') { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === 'true'; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } - //#endregion - - const limit = 10; - const partOf = `${config.url}/users/${userId}/following`; - - if (page) { - const query = { - followerId: user.id, - } as FindOptionsWhere; - - // カーソルが指定されている場合 - if (cursor) { - query.id = LessThan(cursor); - } - - // Get followings - const followings = await Followings.find({ - where: query, - take: limit + 1, - order: { id: -1 }, - }); - - // 「次のページ」があるかどうか - const inStock = followings.length === limit + 1; - if (inStock) followings.pop(); - - const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId))); - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - cursor, - })}`, - user.followingCount, renderedFollowees, partOf, - undefined, - inStock ? `${partOf}?${url.query({ - page: 'true', - cursor: followings[followings.length - 1].id, - })}` : undefined, - ); - - ctx.body = renderActivity(rendered); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts deleted file mode 100644 index 7a2586998a..0000000000 --- a/packages/backend/src/server/activitypub/outbox.ts +++ /dev/null @@ -1,108 +0,0 @@ -import Router from '@koa/router'; -import { Brackets, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; -import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import renderCreate from '@/remote/activitypub/renderer/create.js'; -import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; -import { countIf } from '@/prelude/array.js'; -import * as url from '@/prelude/url.js'; -import { Users, Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { makePaginationQuery } from '../api/common/make-pagination-query.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - const userId = ctx.params.user; - - const sinceId = ctx.request.query.since_id; - if (sinceId != null && typeof sinceId !== 'string') { - ctx.status = 400; - return; - } - - const untilId = ctx.request.query.until_id; - if (untilId != null && typeof untilId !== 'string') { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === 'true'; - - if (countIf(x => x != null, [sinceId, untilId]) > 1) { - ctx.status = 400; - return; - } - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - const limit = 20; - const partOf = `${config.url}/users/${userId}/outbox`; - - if (page) { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) - .andWhere('note.userId = :userId', { userId: user.id }) - .andWhere(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) - .andWhere('note.localOnly = FALSE'); - - const notes = await query.take(limit).getMany(); - - if (sinceId) notes.reverse(); - - const activities = await Promise.all(notes.map(note => packActivity(note))); - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - since_id: sinceId, - until_id: untilId, - })}`, - user.notesCount, activities, partOf, - notes.length ? `${partOf}?${url.query({ - page: 'true', - since_id: notes[0].id, - })}` : undefined, - notes.length ? `${partOf}?${url.query({ - page: 'true', - until_id: notes[notes.length - 1].id, - })}` : undefined, - ); - - ctx.body = renderActivity(rendered); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, user.notesCount, - `${partOf}?page=true`, - `${partOf}?page=true&since_id=000000000000000000000000`, - ); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; - -/** - * Pack Create or Announce Activity - * @param note Note - */ -export async function packActivity(note: Note): Promise { - if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { - const renote = await Notes.findOneByOrFail({ id: note.renoteId }); - return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note); - } - - return renderCreate(await renderNote(note, false), note); -} diff --git a/packages/backend/src/server/api/2fa.ts b/packages/backend/src/server/api/2fa.ts deleted file mode 100644 index 96b9316e47..0000000000 --- a/packages/backend/src/server/api/2fa.ts +++ /dev/null @@ -1,422 +0,0 @@ -import * as crypto from 'node:crypto'; -import * as jsrsasign from 'jsrsasign'; -import config from '@/config/index.js'; - -const ECC_PRELUDE = Buffer.from([0x04]); -const NULL_BYTE = Buffer.from([0]); -const PEM_PRELUDE = Buffer.from( - '3059301306072a8648ce3d020106082a8648ce3d030107034200', - 'hex', -); - -// Android Safetynet attestations are signed with this cert: -const GSR2 = `-----BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 -MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL -v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 -eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq -tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd -C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa -zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB -mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH -V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n -bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG -3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs -J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO -291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS -ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd -AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 -TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== ------END CERTIFICATE-----\n`; - -function base64URLDecode(source: string) { - return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64'); -} - -function getCertSubject(certificate: string) { - const subjectCert = new jsrsasign.X509(); - subjectCert.readCertPEM(certificate); - - const subjectString = subjectCert.getSubjectString(); - const subjectFields = subjectString.slice(1).split('/'); - - const fields = {} as Record; - for (const field of subjectFields) { - const eqIndex = field.indexOf('='); - fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1); - } - - return fields; -} - -function verifyCertificateChain(certificates: string[]) { - let valid = true; - - for (let i = 0; i < certificates.length; i++) { - const Cert = certificates[i]; - const certificate = new jsrsasign.X509(); - certificate.readCertPEM(Cert); - - const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; - - const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); - const algorithm = certificate.getSignatureAlgorithmField(); - const signatureHex = certificate.getSignatureValueHex(); - - // Verify against CA - const Signature = new jsrsasign.KJUR.crypto.Signature({ alg: algorithm }); - Signature.init(CACert); - Signature.updateHex(certStruct); - valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate - } - - return valid; -} - -function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { - if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) { - pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91); - type = 'PUBLIC KEY'; - } - const cert = pemBuffer.toString('base64'); - - const keyParts = []; - const max = Math.ceil(cert.length / 64); - let start = 0; - for (let i = 0; i < max; i++) { - keyParts.push(cert.substring(start, start + 64)); - start += 64; - } - - return ( - `-----BEGIN ${type}-----\n` + - keyParts.join('\n') + - `\n-----END ${type}-----\n` - ); -} - -export function hash(data: Buffer) { - return crypto - .createHash('sha256') - .update(data) - .digest(); -} - -export function verifyLogin({ - publicKey, - authenticatorData, - clientDataJSON, - clientData, - signature, - challenge, -}: { - publicKey: Buffer, - authenticatorData: Buffer, - clientDataJSON: Buffer, - clientData: any, - signature: Buffer, - challenge: string -}) { - if (clientData.type !== 'webauthn.get') { - throw new Error('type is not webauthn.get'); - } - - if (hash(clientData.challenge).toString('hex') !== challenge) { - throw new Error('challenge mismatch'); - } - if (clientData.origin !== config.scheme + '://' + config.host) { - throw new Error('origin mismatch'); - } - - const verificationData = Buffer.concat( - [authenticatorData, hash(clientDataJSON)], - 32 + authenticatorData.length, - ); - - return crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(publicKey), signature); -} - -export const procedures = { - none: { - verify({ publicKey }: { publicKey: Map }) { - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - return { - publicKey: publicKeyU2F, - valid: true, - }; - }, - }, - 'android-key': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - if (attStmt.alg !== -7) { - throw new Error('alg mismatch'); - } - - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash, - ]); - - const attCert: Buffer = attStmt.x5c[0]; - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - if (!attCert.equals(publicKeyData)) { - throw new Error('public key mismatch'); - } - - const isValid = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) - - return { - valid: isValid, - publicKey: publicKeyData, - }; - }, - }, - // what a stupid attestation - 'android-safetynet': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - const verificationData = hash( - Buffer.concat([authenticatorData, clientDataHash]), - ); - - const jwsParts = attStmt.response.toString('utf-8').split('.'); - - const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); - const response = JSON.parse( - base64URLDecode(jwsParts[1]).toString('utf-8'), - ); - const signature = jwsParts[2]; - - if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { - throw new Error('invalid nonce'); - } - - const certificateChain = header.x5c - .map((key: any) => PEMString(key)) - .concat([GSR2]); - - if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { - throw new Error('invalid common name'); - } - - if (!verifyCertificateChain(certificateChain)) { - throw new Error('Invalid certificate chain!'); - } - - const signatureBase = Buffer.from( - jwsParts[0] + '.' + jwsParts[1], - 'utf-8', - ); - - const valid = crypto - .createVerify('sha256') - .update(signatureBase) - .verify(certificateChain[0], base64URLDecode(signature)); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - return { - valid, - publicKey: publicKeyData, - }; - }, - }, - packed: { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash, - ]); - - if (attStmt.x5c) { - const attCert = attStmt.x5c[0]; - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - return { - valid: validSignature, - publicKey: publicKeyData, - }; - } else if (attStmt.ecdaaKeyId) { - // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation - throw new Error('ECDAA-Verify is not supported'); - } else { - if (attStmt.alg !== -7) throw new Error('alg mismatch'); - - throw new Error('self attestation is not supported'); - } - }, - }, - - 'fido-u2f': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map, - rpIdHash: Buffer, - credentialId: Buffer - }) { - const x5c: Buffer[] = attStmt.x5c; - if (x5c.length !== 1) { - throw new Error('x5c length does not match expectation'); - } - - const attCert = x5c[0]; - - // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve - - const negTwo: Buffer = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree: Buffer = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - const verificationData = Buffer.concat([ - NULL_BYTE, - rpIdHash, - clientDataHash, - credentialId, - publicKeyU2F, - ]); - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - return { - valid: validSignature, - publicKey: publicKeyU2F, - }; - }, - }, -}; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts new file mode 100644 index 0000000000..f2ead3d4a1 --- /dev/null +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -0,0 +1,258 @@ +import { performance } from 'perf_hooks'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import type { CacheableLocalUser, User } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type Logger from '@/logger.js'; +import { UserIpsRepository } from '@/models/index.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApiError } from './error.js'; +import { RateLimiterService } from './RateLimiterService.js'; +import { ApiLoggerService } from './ApiLoggerService.js'; +import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; +import type { IEndpointMeta, IEndpoint } from './endpoints.js'; +import type Koa from 'koa'; + +const accessDenied = { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', +}; + +@Injectable() +export class ApiCallService implements OnApplicationShutdown { + #logger: Logger; + #userIpHistories: Map>; + #userIpHistoriesClearIntervalId: NodeJS.Timer; + + constructor( + @Inject(DI.userIpsRepository) + private userIpsRepository: UserIpsRepository, + + private metaService: MetaService, + private authenticateService: AuthenticateService, + private rateLimiterService: RateLimiterService, + private apiLoggerService: ApiLoggerService, + ) { + this.#logger = this.apiLoggerService.logger; + this.#userIpHistories = new Map>(); + + this.#userIpHistoriesClearIntervalId = setInterval(() => { + this.#userIpHistories.clear(); + }, 1000 * 60 * 60); + } + + public handleRequest(endpoint: IEndpoint, exec: any, ctx: Koa.Context) { + return new Promise((res) => { + const body = ctx.is('multipart/form-data') + ? (ctx.request as any).body + : ctx.method === 'GET' + ? ctx.query + : ctx.request.body; + + const reply = (x?: any, y?: ApiError) => { + if (x == null) { + ctx.status = 204; + } else if (typeof x === 'number' && y) { + ctx.status = x; + ctx.body = { + error: { + message: y!.message, + code: y!.code, + id: y!.id, + kind: y!.kind, + ...(y!.info ? { info: y!.info } : {}), + }, + }; + } else { + // 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない + ctx.body = typeof x === 'string' ? JSON.stringify(x) : x; + } + res(); + }; + + // Authentication + this.authenticateService.authenticate(body['i']).then(([user, app]) => { + // API invoking + this.#call(endpoint, exec, user, app, body, ctx).then((res: any) => { + if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { + ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); + } + reply(res); + }).catch((e: ApiError) => { + reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); + }); + + // Log IP + if (user) { + this.metaService.fetch().then(meta => { + if (!meta.enableIpLogging) return; + const ip = ctx.ip; + const ips = this.#userIpHistories.get(user.id); + if (ips == null || !ips.has(ip)) { + if (ips == null) { + this.#userIpHistories.set(user.id, new Set([ip])); + } else { + ips.add(ip); + } + + try { + this.userIpsRepository.createQueryBuilder().insert().values({ + createdAt: new Date(), + userId: user.id, + ip: ip, + }).orIgnore(true).execute(); + } catch { + } + } + }); + } + }).catch(e => { + if (e instanceof AuthenticationError) { + reply(403, new ApiError({ + message: 'Authentication failed. Please ensure your token is correct.', + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + })); + } else { + reply(500, new ApiError()); + } + }); + }); + } + + async #call( + ep: IEndpoint, + exec: any, + user: CacheableLocalUser | null | undefined, + token: AccessToken | null | undefined, + data: any, + ctx?: Koa.Context, + ) { + const isSecure = user != null && token == null; + const isModerator = user != null && (user.isModerator || user.isAdmin); + + if (ep.meta.secure && !isSecure) { + throw new ApiError(accessDenied); + } + + if (ep.meta.limit) { + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + let limitActor: string; + if (user) { + limitActor = user.id; + } else { + limitActor = getIpHash(ctx!.ip); + } + + const limit = Object.assign({}, ep.meta.limit); + + if (!limit.key) { + limit.key = ep.name; + } + + // Rate limit + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(e => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); + }); + } + + 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.requireModerator && !isModerator) { + throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); + } + + if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { + throw new ApiError({ + message: 'Your app does not have the necessary permissions to use this endpoint.', + code: 'PERMISSION_DENIED', + id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + }); + } + + // Cast non JSON input + if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) { + for (const k of Object.keys(ep.params.properties)) { + const param = ep.params.properties![k]; + if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { + try { + data[k] = JSON.parse(data[k]); + } catch (e) { + throw new ApiError({ + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', + }, { + param: k, + reason: `cannot cast to ${param.type}`, + }); + } + } + } + } + + // API invoking + const before = performance.now(); + return await exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((err: Error) => { + if (err instanceof ApiError) { + throw err; + } else { + this.#logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { + ep: ep.name, + ps: data, + e: { + message: err.message, + code: err.name, + stack: err.stack, + }, + }); + console.error(err); + throw new ApiError(null, { + e: { + message: err.message, + code: err.name, + stack: err.stack, + }, + }); + } + }).finally(() => { + const after = performance.now(); + const time = after - before; + if (time > 1000) { + this.#logger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`); + } + }); + } + + 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 new file mode 100644 index 0000000000..c8c5fec85c --- /dev/null +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Logger from '@/logger.js'; + +@Injectable() +export class ApiLoggerService { + public logger: Logger; + + constructor( + ) { + this.logger = new Logger('api'); + } +} diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts new file mode 100644 index 0000000000..cfe39238dd --- /dev/null +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -0,0 +1,160 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Koa from 'koa'; +import Router from '@koa/router'; +import multer from '@koa/multer'; +import bodyParser from 'koa-bodyparser'; +import cors from '@koa/cors'; +import { ModuleRef } from '@nestjs/core'; +import { Config } from '@/config.js'; +import { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import endpoints from './endpoints.js'; +import { ApiCallService } from './ApiCallService.js'; +import { SignupApiService } from './SignupApiService.js'; +import { SigninApiService } from './SigninApiService.js'; +import { GithubServerService } from './integration/GithubServerService.js'; +import { DiscordServerService } from './integration/DiscordServerService.js'; +import { TwitterServerService } from './integration/TwitterServerService.js'; + +@Injectable() +export class ApiServerService { + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + private userEntityService: UserEntityService, + private apiCallService: ApiCallService, + private signupApiServiceService: SignupApiService, + private signinApiServiceService: SigninApiService, + private githubServerService: GithubServerService, + private discordServerService: DiscordServerService, + private twitterServerService: TwitterServerService, + ) { + } + + public createApiServer() { + const handlers: Record = {}; + + for (const endpoint of endpoints) { + handlers[endpoint.name] = this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec; + } + + // Init app + const apiServer = new Koa(); + + apiServer.use(cors({ + origin: '*', + })); + + // No caching + apiServer.use(async (ctx, next) => { + ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); + await next(); + }); + + apiServer.use(bodyParser({ + // リクエストが multipart/form-data でない限りはJSONだと見なす + detectJSON: ctx => !ctx.is('multipart/form-data'), + })); + + // Init multer instance + const upload = multer({ + storage: multer.diskStorage({}), + limits: { + fileSize: this.config.maxFileSize ?? 262144000, + files: 1, + }, + }); + + // Init router + const router = new Router(); + + /** + * Register endpoint handlers + */ + for (const endpoint of endpoints) { + if (endpoint.meta.requireFile) { + router.post(`/${endpoint.name}`, upload.single('file'), this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name])); + } else { + // 後方互換性のため + if (endpoint.name.includes('-')) { + router.post(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name])); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name])); + } else { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; }); + } + } + + router.post(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name])); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name])); + } else { + router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; }); + } + } + } + + router.post('/signup', ctx => this.signupApiServiceService.signup(ctx)); + router.post('/signin', ctx => this.signinApiServiceService.signin(ctx)); + router.post('/signup-pending', ctx => this.signupApiServiceService.signupPending(ctx)); + + router.use(this.discordServerService.create().routes()); + router.use(this.githubServerService.create().routes()); + router.use(this.twitterServerService.create().routes()); + + router.get('/v1/instance/peers', async ctx => { + const instances = await this.instancesRepository.find({ + select: ['host'], + }); + + ctx.body = instances.map(instance => instance.host); + }); + + router.post('/miauth/:session/check', async ctx => { + const token = await this.accessTokensRepository.findOneBy({ + session: ctx.params.session, + }); + + if (token && token.session != null && !token.fetched) { + this.accessTokensRepository.update(token.id, { + fetched: true, + }); + + ctx.body = { + ok: true, + token: token.token, + user: await this.userEntityService.pack(token.userId, null, { detail: true }), + }; + } else { + ctx.body = { + ok: false, + }; + } + }); + + // Return 404 for unknown API + router.all('(.*)', async ctx => { + ctx.status = 404; + }); + + // Register router + apiServer.use(router.routes()); + + return apiServer; + } +} diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts new file mode 100644 index 0000000000..b8bd09509a --- /dev/null +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -0,0 +1,86 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; +import type { CacheableLocalUser, ILocalUser } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; +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'; + +export class AuthenticationError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuthenticationError'; + } +} + +@Injectable() +export class AuthenticateService { + #appCache: Cache; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private userCacheService: UserCacheService, + ) { + this.#appCache = new Cache(Infinity); + } + + public async authenticate(token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> { + if (token == null) { + return [null, null]; + } + + if (isNativeToken(token)) { + const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token, + () => this.usersRepository.findOneBy({ token }) as Promise); + + if (user == null) { + throw new AuthenticationError('user not found'); + } + + return [user, null]; + } else { + const accessToken = await this.accessTokensRepository.findOne({ + where: [{ + hash: token.toLowerCase(), // app + }, { + token: token, // miauth + }], + }); + + if (accessToken == null) { + throw new AuthenticationError('invalid signature'); + } + + this.accessTokensRepository.update(accessToken.id, { + lastUsedAt: new Date(), + }); + + const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId, + () => this.usersRepository.findOneBy({ + id: accessToken.userId, + }) as Promise); + + if (accessToken.appId) { + const app = await this.#appCache.fetch(accessToken.appId, + () => this.appsRepository.findOneByOrFail({ id: accessToken.appId! })); + + return [user, { + id: accessToken.id, + permission: app.permission, + } as AccessToken]; + } else { + return [user, accessToken]; + } + } + } +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts new file mode 100644 index 0000000000..d2dfd78fd4 --- /dev/null +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -0,0 +1,1268 @@ +import { Module } from '@nestjs/common'; + +import { CoreModule } from '@/core/CoreModule.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; +import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; +import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; +import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; +import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; +import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; +import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; +import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; +import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; +import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; +import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; +import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; +import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; +import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; +import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; +import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; +import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; +import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; +import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; +import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; +import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; +import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; +import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; +import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; +import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; +import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; +import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; +import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; +import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; +import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; +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'; +import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; +import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; +import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; +import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; +import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; +import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; +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___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'; +import * as ep___antennas_list from './endpoints/antennas/list.js'; +import * as ep___antennas_notes from './endpoints/antennas/notes.js'; +import * as ep___antennas_show from './endpoints/antennas/show.js'; +import * as ep___antennas_update from './endpoints/antennas/update.js'; +import * as ep___ap_get from './endpoints/ap/get.js'; +import * as ep___ap_show from './endpoints/ap/show.js'; +import * as ep___app_create from './endpoints/app/create.js'; +import * as ep___app_show from './endpoints/app/show.js'; +import * as ep___auth_accept from './endpoints/auth/accept.js'; +import * as ep___auth_session_generate from './endpoints/auth/session/generate.js'; +import * as ep___auth_session_show from './endpoints/auth/session/show.js'; +import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'; +import * as ep___blocking_create from './endpoints/blocking/create.js'; +import * as ep___blocking_delete from './endpoints/blocking/delete.js'; +import * as ep___blocking_list from './endpoints/blocking/list.js'; +import * as ep___channels_create from './endpoints/channels/create.js'; +import * as ep___channels_featured from './endpoints/channels/featured.js'; +import * as ep___channels_follow from './endpoints/channels/follow.js'; +import * as ep___channels_followed from './endpoints/channels/followed.js'; +import * as ep___channels_owned from './endpoints/channels/owned.js'; +import * as ep___channels_show from './endpoints/channels/show.js'; +import * as ep___channels_timeline from './endpoints/channels/timeline.js'; +import * as ep___channels_unfollow from './endpoints/channels/unfollow.js'; +import * as ep___channels_update from './endpoints/channels/update.js'; +import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; +import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; +import * as ep___charts_drive from './endpoints/charts/drive.js'; +import * as ep___charts_federation from './endpoints/charts/federation.js'; +import * as ep___charts_hashtag from './endpoints/charts/hashtag.js'; +import * as ep___charts_instance from './endpoints/charts/instance.js'; +import * as ep___charts_notes from './endpoints/charts/notes.js'; +import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; +import * as ep___charts_user_following from './endpoints/charts/user/following.js'; +import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; +import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; +import * as ep___charts_users from './endpoints/charts/users.js'; +import * as ep___clips_addNote from './endpoints/clips/add-note.js'; +import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; +import * as ep___clips_create from './endpoints/clips/create.js'; +import * as ep___clips_delete from './endpoints/clips/delete.js'; +import * as ep___clips_list from './endpoints/clips/list.js'; +import * as ep___clips_notes from './endpoints/clips/notes.js'; +import * as ep___clips_show from './endpoints/clips/show.js'; +import * as ep___clips_update from './endpoints/clips/update.js'; +import * as ep___drive from './endpoints/drive.js'; +import * as ep___drive_files from './endpoints/drive/files.js'; +import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; +import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; +import * as ep___drive_files_create from './endpoints/drive/files/create.js'; +import * as ep___drive_files_delete from './endpoints/drive/files/delete.js'; +import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js'; +import * as ep___drive_files_find from './endpoints/drive/files/find.js'; +import * as ep___drive_files_show from './endpoints/drive/files/show.js'; +import * as ep___drive_files_update from './endpoints/drive/files/update.js'; +import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js'; +import * as ep___drive_folders from './endpoints/drive/folders.js'; +import * as ep___drive_folders_create from './endpoints/drive/folders/create.js'; +import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js'; +import * as ep___drive_folders_find from './endpoints/drive/folders/find.js'; +import * as ep___drive_folders_show from './endpoints/drive/folders/show.js'; +import * as ep___drive_folders_update from './endpoints/drive/folders/update.js'; +import * as ep___drive_stream from './endpoints/drive/stream.js'; +import * as ep___emailAddress_available from './endpoints/email-address/available.js'; +import * as ep___endpoint from './endpoints/endpoint.js'; +import * as ep___endpoints from './endpoints/endpoints.js'; +import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js'; +import * as ep___federation_followers from './endpoints/federation/followers.js'; +import * as ep___federation_following from './endpoints/federation/following.js'; +import * as ep___federation_instances from './endpoints/federation/instances.js'; +import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; +import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; +import * as ep___federation_users from './endpoints/federation/users.js'; +import * as ep___federation_stats from './endpoints/federation/stats.js'; +import * as ep___following_create from './endpoints/following/create.js'; +import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_invalidate from './endpoints/following/invalidate.js'; +import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; +import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; +import * as ep___following_requests_list from './endpoints/following/requests/list.js'; +import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; +import * as ep___gallery_featured from './endpoints/gallery/featured.js'; +import * as ep___gallery_popular from './endpoints/gallery/popular.js'; +import * as ep___gallery_posts from './endpoints/gallery/posts.js'; +import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; +import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; +import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; +import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; +import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; +import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; +import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; +import * as ep___hashtags_list from './endpoints/hashtags/list.js'; +import * as ep___hashtags_search from './endpoints/hashtags/search.js'; +import * as ep___hashtags_show from './endpoints/hashtags/show.js'; +import * as ep___hashtags_trend from './endpoints/hashtags/trend.js'; +import * as ep___hashtags_users from './endpoints/hashtags/users.js'; +import * as ep___i from './endpoints/i.js'; +import * as ep___i_2fa_done from './endpoints/i/2fa/done.js'; +import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; +import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; +import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; +import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; +import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; +import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; +import * as ep___i_apps from './endpoints/i/apps.js'; +import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_changePassword from './endpoints/i/change-password.js'; +import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; +import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; +import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; +import * as ep___i_exportMute from './endpoints/i/export-mute.js'; +import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; +import * as ep___i_favorites from './endpoints/i/favorites.js'; +import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; +import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; +import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; +import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; +import * as ep___i_importFollowing from './endpoints/i/import-following.js'; +import * as ep___i_importMuting from './endpoints/i/import-muting.js'; +import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; +import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; +import * as ep___i_pages from './endpoints/i/pages.js'; +import * as ep___i_pin from './endpoints/i/pin.js'; +import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js'; +import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; +import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; +import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; +import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js'; +import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js'; +import * as ep___i_registry_get from './endpoints/i/registry/get.js'; +import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; +import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; +import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; +import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_set from './endpoints/i/registry/set.js'; +import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; +import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; +import * as ep___i_unpin from './endpoints/i/unpin.js'; +import * as ep___i_updateEmail from './endpoints/i/update-email.js'; +import * as ep___i_update from './endpoints/i/update.js'; +import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js'; +import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; +import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; +import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; +import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; +import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___messaging_history from './endpoints/messaging/history.js'; +import * as ep___messaging_messages from './endpoints/messaging/messages.js'; +import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; +import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; +import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; +import * as ep___meta from './endpoints/meta.js'; +import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; +import * as ep___mute_create from './endpoints/mute/create.js'; +import * as ep___mute_delete from './endpoints/mute/delete.js'; +import * as ep___mute_list from './endpoints/mute/list.js'; +import * as ep___my_apps from './endpoints/my/apps.js'; +import * as ep___notes from './endpoints/notes.js'; +import * as ep___notes_children from './endpoints/notes/children.js'; +import * as ep___notes_clips from './endpoints/notes/clips.js'; +import * as ep___notes_conversation from './endpoints/notes/conversation.js'; +import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; +import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; +import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; +import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; +import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; +import * as ep___notes_mentions from './endpoints/notes/mentions.js'; +import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; +import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; +import * as ep___notes_reactions from './endpoints/notes/reactions.js'; +import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; +import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; +import * as ep___notes_renotes from './endpoints/notes/renotes.js'; +import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; +import * as ep___notes_search from './endpoints/notes/search.js'; +import * as ep___notes_show from './endpoints/notes/show.js'; +import * as ep___notes_state from './endpoints/notes/state.js'; +import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; +import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; +import * as ep___notes_timeline from './endpoints/notes/timeline.js'; +import * as ep___notes_translate from './endpoints/notes/translate.js'; +import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; +import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; +import * as ep___notifications_create from './endpoints/notifications/create.js'; +import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___notifications_read from './endpoints/notifications/read.js'; +import * as ep___pagePush from './endpoints/page-push.js'; +import * as ep___pages_create from './endpoints/pages/create.js'; +import * as ep___pages_delete from './endpoints/pages/delete.js'; +import * as ep___pages_featured from './endpoints/pages/featured.js'; +import * as ep___pages_like from './endpoints/pages/like.js'; +import * as ep___pages_show from './endpoints/pages/show.js'; +import * as ep___pages_unlike from './endpoints/pages/unlike.js'; +import * as ep___pages_update from './endpoints/pages/update.js'; +import * as ep___ping from './endpoints/ping.js'; +import * as ep___pinnedUsers from './endpoints/pinned-users.js'; +import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; +import * as ep___resetDb from './endpoints/reset-db.js'; +import * as ep___resetPassword from './endpoints/reset-password.js'; +import * as ep___serverInfo from './endpoints/server-info.js'; +import * as ep___stats from './endpoints/stats.js'; +import * as ep___sw_register from './endpoints/sw/register.js'; +import * as ep___sw_unregister from './endpoints/sw/unregister.js'; +import * as ep___test from './endpoints/test.js'; +import * as ep___username_available from './endpoints/username/available.js'; +import * as ep___users from './endpoints/users.js'; +import * as ep___users_clips from './endpoints/users/clips.js'; +import * as ep___users_followers from './endpoints/users/followers.js'; +import * as ep___users_following from './endpoints/users/following.js'; +import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; +import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_groups_create from './endpoints/users/groups/create.js'; +import * as ep___users_groups_delete from './endpoints/users/groups/delete.js'; +import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js'; +import * as ep___users_groups_invitations_reject from './endpoints/users/groups/invitations/reject.js'; +import * as ep___users_groups_invite from './endpoints/users/groups/invite.js'; +import * as ep___users_groups_joined from './endpoints/users/groups/joined.js'; +import * as ep___users_groups_leave from './endpoints/users/groups/leave.js'; +import * as ep___users_groups_owned from './endpoints/users/groups/owned.js'; +import * as ep___users_groups_pull from './endpoints/users/groups/pull.js'; +import * as ep___users_groups_show from './endpoints/users/groups/show.js'; +import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.js'; +import * as ep___users_groups_update from './endpoints/users/groups/update.js'; +import * as ep___users_lists_create from './endpoints/users/lists/create.js'; +import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; +import * as ep___users_lists_list from './endpoints/users/lists/list.js'; +import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; +import * as ep___users_lists_push from './endpoints/users/lists/push.js'; +import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_notes from './endpoints/users/notes.js'; +import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_reactions from './endpoints/users/reactions.js'; +import * as ep___users_recommendation from './endpoints/users/recommendation.js'; +import * as ep___users_relation from './endpoints/users/relation.js'; +import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; +import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; +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 { GetterService } from './common/GetterService.js'; +import { ApiLoggerService } from './ApiLoggerService.js'; +import type { Provider } from '@nestjs/common'; + +const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; +const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; +const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; +const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default }; +const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default }; +const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default }; +const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default }; +const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep___admin_ad_update.default }; +const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default }; +const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; +const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; +const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; +const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; +const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; +const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; +const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default }; +const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default }; +const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; +const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; +const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; +const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; +const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default }; +const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default }; +const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default }; +const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default }; +const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default }; +const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default }; +const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default }; +const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; +const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; +const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; +const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default }; +const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; +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 }; +const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default }; +const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default }; +const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default }; +const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; +const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; +const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; +const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; +const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; +const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; +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 $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 }; +const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default }; +const $antennas_notes: Provider = { provide: 'ep:antennas/notes', useClass: ep___antennas_notes.default }; +const $antennas_show: Provider = { provide: 'ep:antennas/show', useClass: ep___antennas_show.default }; +const $antennas_update: Provider = { provide: 'ep:antennas/update', useClass: ep___antennas_update.default }; +const $ap_get: Provider = { provide: 'ep:ap/get', useClass: ep___ap_get.default }; +const $ap_show: Provider = { provide: 'ep:ap/show', useClass: ep___ap_show.default }; +const $app_create: Provider = { provide: 'ep:app/create', useClass: ep___app_create.default }; +const $app_show: Provider = { provide: 'ep:app/show', useClass: ep___app_show.default }; +const $auth_accept: Provider = { provide: 'ep:auth/accept', useClass: ep___auth_accept.default }; +const $auth_session_generate: Provider = { provide: 'ep:auth/session/generate', useClass: ep___auth_session_generate.default }; +const $auth_session_show: Provider = { provide: 'ep:auth/session/show', useClass: ep___auth_session_show.default }; +const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', useClass: ep___auth_session_userkey.default }; +const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default }; +const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default }; +const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default }; +const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default }; +const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default }; +const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default }; +const $channels_followed: Provider = { provide: 'ep:channels/followed', useClass: ep___channels_followed.default }; +const $channels_owned: Provider = { provide: 'ep:channels/owned', useClass: ep___channels_owned.default }; +const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___channels_show.default }; +const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default }; +const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default }; +const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default }; +const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default }; +const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; +const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; +const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default }; +const $charts_hashtag: Provider = { provide: 'ep:charts/hashtag', useClass: ep___charts_hashtag.default }; +const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default }; +const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default }; +const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default }; +const $charts_user_following: Provider = { provide: 'ep:charts/user/following', useClass: ep___charts_user_following.default }; +const $charts_user_notes: Provider = { provide: 'ep:charts/user/notes', useClass: ep___charts_user_notes.default }; +const $charts_user_reactions: Provider = { provide: 'ep:charts/user/reactions', useClass: ep___charts_user_reactions.default }; +const $charts_users: Provider = { provide: 'ep:charts/users', useClass: ep___charts_users.default }; +const $clips_addNote: Provider = { provide: 'ep:clips/add-note', useClass: ep___clips_addNote.default }; +const $clips_removeNote: Provider = { provide: 'ep:clips/remove-note', useClass: ep___clips_removeNote.default }; +const $clips_create: Provider = { provide: 'ep:clips/create', useClass: ep___clips_create.default }; +const $clips_delete: Provider = { provide: 'ep:clips/delete', useClass: ep___clips_delete.default }; +const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_list.default }; +const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default }; +const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default }; +const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default }; +const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default }; +const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default }; +const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default }; +const $drive_files_checkExistence: Provider = { provide: 'ep:drive/files/check-existence', useClass: ep___drive_files_checkExistence.default }; +const $drive_files_create: Provider = { provide: 'ep:drive/files/create', useClass: ep___drive_files_create.default }; +const $drive_files_delete: Provider = { provide: 'ep:drive/files/delete', useClass: ep___drive_files_delete.default }; +const $drive_files_findByHash: Provider = { provide: 'ep:drive/files/find-by-hash', useClass: ep___drive_files_findByHash.default }; +const $drive_files_find: Provider = { provide: 'ep:drive/files/find', useClass: ep___drive_files_find.default }; +const $drive_files_show: Provider = { provide: 'ep:drive/files/show', useClass: ep___drive_files_show.default }; +const $drive_files_update: Provider = { provide: 'ep:drive/files/update', useClass: ep___drive_files_update.default }; +const $drive_files_uploadFromUrl: Provider = { provide: 'ep:drive/files/upload-from-url', useClass: ep___drive_files_uploadFromUrl.default }; +const $drive_folders: Provider = { provide: 'ep:drive/folders', useClass: ep___drive_folders.default }; +const $drive_folders_create: Provider = { provide: 'ep:drive/folders/create', useClass: ep___drive_folders_create.default }; +const $drive_folders_delete: Provider = { provide: 'ep:drive/folders/delete', useClass: ep___drive_folders_delete.default }; +const $drive_folders_find: Provider = { provide: 'ep:drive/folders/find', useClass: ep___drive_folders_find.default }; +const $drive_folders_show: Provider = { provide: 'ep:drive/folders/show', useClass: ep___drive_folders_show.default }; +const $drive_folders_update: Provider = { provide: 'ep:drive/folders/update', useClass: ep___drive_folders_update.default }; +const $drive_stream: Provider = { provide: 'ep:drive/stream', useClass: ep___drive_stream.default }; +const $emailAddress_available: Provider = { provide: 'ep:email-address/available', useClass: ep___emailAddress_available.default }; +const $endpoint: Provider = { provide: 'ep:endpoint', useClass: ep___endpoint.default }; +const $endpoints: Provider = { provide: 'ep:endpoints', useClass: ep___endpoints.default }; +const $exportCustomEmojis: Provider = { provide: 'ep:export-custom-emojis', useClass: ep___exportCustomEmojis.default }; +const $federation_followers: Provider = { provide: 'ep:federation/followers', useClass: ep___federation_followers.default }; +const $federation_following: Provider = { provide: 'ep:federation/following', useClass: ep___federation_following.default }; +const $federation_instances: Provider = { provide: 'ep:federation/instances', useClass: ep___federation_instances.default }; +const $federation_showInstance: Provider = { provide: 'ep:federation/show-instance', useClass: ep___federation_showInstance.default }; +const $federation_updateRemoteUser: Provider = { provide: 'ep:federation/update-remote-user', useClass: ep___federation_updateRemoteUser.default }; +const $federation_users: Provider = { provide: 'ep:federation/users', useClass: ep___federation_users.default }; +const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; +const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; +const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; +const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; +const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; +const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; +const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default }; +const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default }; +const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default }; +const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default }; +const $gallery_posts: Provider = { provide: 'ep:gallery/posts', useClass: ep___gallery_posts.default }; +const $gallery_posts_create: Provider = { provide: 'ep:gallery/posts/create', useClass: ep___gallery_posts_create.default }; +const $gallery_posts_delete: Provider = { provide: 'ep:gallery/posts/delete', useClass: ep___gallery_posts_delete.default }; +const $gallery_posts_like: Provider = { provide: 'ep:gallery/posts/like', useClass: ep___gallery_posts_like.default }; +const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useClass: ep___gallery_posts_show.default }; +const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default }; +const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default }; +const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default }; +const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default }; +const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default }; +const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default }; +const $hashtags_trend: Provider = { provide: 'ep:hashtags/trend', useClass: ep___hashtags_trend.default }; +const $hashtags_users: Provider = { provide: 'ep:hashtags/users', useClass: ep___hashtags_users.default }; +const $i: Provider = { provide: 'ep:i', useClass: ep___i.default }; +const $i_2fa_done: Provider = { provide: 'ep:i/2fa/done', useClass: ep___i_2fa_done.default }; +const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___i_2fa_keyDone.default }; +const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default }; +const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default }; +const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default }; +const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default }; +const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; +const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; +const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; +const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; +const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; +const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; +const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; +const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; +const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; +const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; +const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; +const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; +const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default }; +const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default }; +const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; +const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; +const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; +const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; +const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; +const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; +const $i_readAllMessagingMessages: Provider = { provide: 'ep:i/read-all-messaging-messages', useClass: ep___i_readAllMessagingMessages.default }; +const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default }; +const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default }; +const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default }; +const $i_registry_getAll: Provider = { provide: 'ep:i/registry/get-all', useClass: ep___i_registry_getAll.default }; +const $i_registry_getDetail: Provider = { provide: 'ep:i/registry/get-detail', useClass: ep___i_registry_getDetail.default }; +const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep___i_registry_get.default }; +const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; +const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default }; +const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default }; +const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default }; +const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default }; +const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default }; +const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default }; +const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; +const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; +const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; +const $i_userGroupInvites: Provider = { provide: 'ep:i/user-group-invites', useClass: ep___i_userGroupInvites.default }; +const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; +const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; +const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; +const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; +const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; +const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default }; +const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default }; +const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default }; +const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; +const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; +const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; +const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; +const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; +const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; +const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default }; +const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default }; +const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default }; +const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default }; +const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; +const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; +const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; +const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; +const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; +const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; +const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; +const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; +const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; +const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; +const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; +const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; +const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; +const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default }; +const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default }; +const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default }; +const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default }; +const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default }; +const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default }; +const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default }; +const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default }; +const $notes_state: Provider = { provide: 'ep:notes/state', useClass: ep___notes_state.default }; +const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/create', useClass: ep___notes_threadMuting_create.default }; +const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default }; +const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default }; +const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default }; +const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default }; +const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; +const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; +const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; +const $notifications_read: Provider = { provide: 'ep:notifications/read', useClass: ep___notifications_read.default }; +const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; +const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; +const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; +const $pages_featured: Provider = { provide: 'ep:pages/featured', useClass: ep___pages_featured.default }; +const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_like.default }; +const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default }; +const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default }; +const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default }; +const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default }; +const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default }; +const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default }; +const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default }; +const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default }; +const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default }; +const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default }; +const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default }; +const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default }; +const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default }; +const $test: Provider = { provide: 'ep:test', useClass: ep___test.default }; +const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default }; +const $users: Provider = { provide: 'ep:users', useClass: ep___users.default }; +const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default }; +const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default }; +const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; +const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; +const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; +const $users_groups_create: Provider = { provide: 'ep:users/groups/create', useClass: ep___users_groups_create.default }; +const $users_groups_delete: Provider = { provide: 'ep:users/groups/delete', useClass: ep___users_groups_delete.default }; +const $users_groups_invitations_accept: Provider = { provide: 'ep:users/groups/invitations/accept', useClass: ep___users_groups_invitations_accept.default }; +const $users_groups_invitations_reject: Provider = { provide: 'ep:users/groups/invitations/reject', useClass: ep___users_groups_invitations_reject.default }; +const $users_groups_invite: Provider = { provide: 'ep:users/groups/invite', useClass: ep___users_groups_invite.default }; +const $users_groups_joined: Provider = { provide: 'ep:users/groups/joined', useClass: ep___users_groups_joined.default }; +const $users_groups_leave: Provider = { provide: 'ep:users/groups/leave', useClass: ep___users_groups_leave.default }; +const $users_groups_owned: Provider = { provide: 'ep:users/groups/owned', useClass: ep___users_groups_owned.default }; +const $users_groups_pull: Provider = { provide: 'ep:users/groups/pull', useClass: ep___users_groups_pull.default }; +const $users_groups_show: Provider = { provide: 'ep:users/groups/show', useClass: ep___users_groups_show.default }; +const $users_groups_transfer: Provider = { provide: 'ep:users/groups/transfer', useClass: ep___users_groups_transfer.default }; +const $users_groups_update: Provider = { provide: 'ep:users/groups/update', useClass: ep___users_groups_update.default }; +const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; +const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; +const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; +const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default }; +const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; +const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; +const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; +const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; +const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; +const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; +const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default }; +const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default }; +const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClass: ep___users_reportAbuse.default }; +const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; +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 }; + +@Module({ + imports: [ + CoreModule, + ], + providers: [ + GetterService, + ApiLoggerService, + $admin_meta, + $admin_abuseUserReports, + $admin_accounts_create, + $admin_accounts_delete, + $admin_ad_create, + $admin_ad_delete, + $admin_ad_list, + $admin_ad_update, + $admin_announcements_create, + $admin_announcements_delete, + $admin_announcements_list, + $admin_announcements_update, + $admin_deleteAllFilesOfAUser, + $admin_drive_cleanRemoteFiles, + $admin_drive_cleanup, + $admin_drive_files, + $admin_drive_showFile, + $admin_emoji_addAliasesBulk, + $admin_emoji_add, + $admin_emoji_copy, + $admin_emoji_deleteBulk, + $admin_emoji_delete, + $admin_emoji_importZip, + $admin_emoji_listRemote, + $admin_emoji_list, + $admin_emoji_removeAliasesBulk, + $admin_emoji_setAliasesBulk, + $admin_emoji_setCategoryBulk, + $admin_emoji_update, + $admin_federation_deleteAllFiles, + $admin_federation_refreshRemoteInstanceMetadata, + $admin_federation_removeAllFollowing, + $admin_federation_updateInstance, + $admin_getIndexStats, + $admin_getTableStats, + $admin_getUserIps, + $admin_invite, + $admin_moderators_add, + $admin_moderators_remove, + $admin_promo_create, + $admin_queue_clear, + $admin_queue_deliverDelayed, + $admin_queue_inboxDelayed, + $admin_queue_stats, + $admin_relays_add, + $admin_relays_list, + $admin_relays_remove, + $admin_resetPassword, + $admin_resolveAbuseUserReport, + $admin_sendEmail, + $admin_serverInfo, + $admin_showModerationLogs, + $admin_showUser, + $admin_showUsers, + $admin_silenceUser, + $admin_suspendUser, + $admin_unsilenceUser, + $admin_unsuspendUser, + $admin_updateMeta, + $admin_deleteAccount, + $admin_updateUserNote, + $announcements, + $antennas_create, + $antennas_delete, + $antennas_list, + $antennas_notes, + $antennas_show, + $antennas_update, + $ap_get, + $ap_show, + $app_create, + $app_show, + $auth_accept, + $auth_session_generate, + $auth_session_show, + $auth_session_userkey, + $blocking_create, + $blocking_delete, + $blocking_list, + $channels_create, + $channels_featured, + $channels_follow, + $channels_followed, + $channels_owned, + $channels_show, + $channels_timeline, + $channels_unfollow, + $channels_update, + $charts_activeUsers, + $charts_apRequest, + $charts_drive, + $charts_federation, + $charts_hashtag, + $charts_instance, + $charts_notes, + $charts_user_drive, + $charts_user_following, + $charts_user_notes, + $charts_user_reactions, + $charts_users, + $clips_addNote, + $clips_removeNote, + $clips_create, + $clips_delete, + $clips_list, + $clips_notes, + $clips_show, + $clips_update, + $drive, + $drive_files, + $drive_files_attachedNotes, + $drive_files_checkExistence, + $drive_files_create, + $drive_files_delete, + $drive_files_findByHash, + $drive_files_find, + $drive_files_show, + $drive_files_update, + $drive_files_uploadFromUrl, + $drive_folders, + $drive_folders_create, + $drive_folders_delete, + $drive_folders_find, + $drive_folders_show, + $drive_folders_update, + $drive_stream, + $emailAddress_available, + $endpoint, + $endpoints, + $exportCustomEmojis, + $federation_followers, + $federation_following, + $federation_instances, + $federation_showInstance, + $federation_updateRemoteUser, + $federation_users, + $federation_stats, + $following_create, + $following_delete, + $following_invalidate, + $following_requests_accept, + $following_requests_cancel, + $following_requests_list, + $following_requests_reject, + $gallery_featured, + $gallery_popular, + $gallery_posts, + $gallery_posts_create, + $gallery_posts_delete, + $gallery_posts_like, + $gallery_posts_show, + $gallery_posts_unlike, + $gallery_posts_update, + $getOnlineUsersCount, + $hashtags_list, + $hashtags_search, + $hashtags_show, + $hashtags_trend, + $hashtags_users, + $i, + $i_2fa_done, + $i_2fa_keyDone, + $i_2fa_passwordLess, + $i_2fa_registerKey, + $i_2fa_register, + $i_2fa_removeKey, + $i_2fa_unregister, + $i_apps, + $i_authorizedApps, + $i_changePassword, + $i_deleteAccount, + $i_exportBlocking, + $i_exportFollowing, + $i_exportMute, + $i_exportNotes, + $i_exportUserLists, + $i_favorites, + $i_gallery_likes, + $i_gallery_posts, + $i_getWordMutedNotesCount, + $i_importBlocking, + $i_importFollowing, + $i_importMuting, + $i_importUserLists, + $i_notifications, + $i_pageLikes, + $i_pages, + $i_pin, + $i_readAllMessagingMessages, + $i_readAllUnreadNotes, + $i_readAnnouncement, + $i_regenerateToken, + $i_registry_getAll, + $i_registry_getDetail, + $i_registry_get, + $i_registry_keysWithType, + $i_registry_keys, + $i_registry_remove, + $i_registry_scopes, + $i_registry_set, + $i_revokeToken, + $i_signinHistory, + $i_unpin, + $i_updateEmail, + $i_update, + $i_userGroupInvites, + $i_webhooks_create, + $i_webhooks_list, + $i_webhooks_show, + $i_webhooks_update, + $i_webhooks_delete, + $messaging_history, + $messaging_messages, + $messaging_messages_create, + $messaging_messages_delete, + $messaging_messages_read, + $meta, + $miauth_genToken, + $mute_create, + $mute_delete, + $mute_list, + $my_apps, + $notes, + $notes_children, + $notes_clips, + $notes_conversation, + $notes_create, + $notes_delete, + $notes_favorites_create, + $notes_favorites_delete, + $notes_featured, + $notes_globalTimeline, + $notes_hybridTimeline, + $notes_localTimeline, + $notes_mentions, + $notes_polls_recommendation, + $notes_polls_vote, + $notes_reactions, + $notes_reactions_create, + $notes_reactions_delete, + $notes_renotes, + $notes_replies, + $notes_searchByTag, + $notes_search, + $notes_show, + $notes_state, + $notes_threadMuting_create, + $notes_threadMuting_delete, + $notes_timeline, + $notes_translate, + $notes_unrenote, + $notes_userListTimeline, + $notifications_create, + $notifications_markAllAsRead, + $notifications_read, + $pagePush, + $pages_create, + $pages_delete, + $pages_featured, + $pages_like, + $pages_show, + $pages_unlike, + $pages_update, + $ping, + $pinnedUsers, + $promo_read, + $requestResetPassword, + $resetDb, + $resetPassword, + $serverInfo, + $stats, + $sw_register, + $sw_unregister, + $test, + $username_available, + $users, + $users_clips, + $users_followers, + $users_following, + $users_gallery_posts, + $users_getFrequentlyRepliedUsers, + $users_groups_create, + $users_groups_delete, + $users_groups_invitations_accept, + $users_groups_invitations_reject, + $users_groups_invite, + $users_groups_joined, + $users_groups_leave, + $users_groups_owned, + $users_groups_pull, + $users_groups_show, + $users_groups_transfer, + $users_groups_update, + $users_lists_create, + $users_lists_delete, + $users_lists_list, + $users_lists_pull, + $users_lists_push, + $users_lists_show, + $users_lists_update, + $users_notes, + $users_pages, + $users_reactions, + $users_recommendation, + $users_relation, + $users_reportAbuse, + $users_searchByUsernameAndHost, + $users_search, + $users_show, + $users_stats, + $admin_driveCapOverride, + $fetchRss, + ], + exports: [ + $admin_meta, + $admin_abuseUserReports, + $admin_accounts_create, + $admin_accounts_delete, + $admin_ad_create, + $admin_ad_delete, + $admin_ad_list, + $admin_ad_update, + $admin_announcements_create, + $admin_announcements_delete, + $admin_announcements_list, + $admin_announcements_update, + $admin_deleteAllFilesOfAUser, + $admin_drive_cleanRemoteFiles, + $admin_drive_cleanup, + $admin_drive_files, + $admin_drive_showFile, + $admin_emoji_addAliasesBulk, + $admin_emoji_add, + $admin_emoji_copy, + $admin_emoji_deleteBulk, + $admin_emoji_delete, + $admin_emoji_importZip, + $admin_emoji_listRemote, + $admin_emoji_list, + $admin_emoji_removeAliasesBulk, + $admin_emoji_setAliasesBulk, + $admin_emoji_setCategoryBulk, + $admin_emoji_update, + $admin_federation_deleteAllFiles, + $admin_federation_refreshRemoteInstanceMetadata, + $admin_federation_removeAllFollowing, + $admin_federation_updateInstance, + $admin_getIndexStats, + $admin_getTableStats, + $admin_getUserIps, + $admin_invite, + $admin_moderators_add, + $admin_moderators_remove, + $admin_promo_create, + $admin_queue_clear, + $admin_queue_deliverDelayed, + $admin_queue_inboxDelayed, + $admin_queue_stats, + $admin_relays_add, + $admin_relays_list, + $admin_relays_remove, + $admin_resetPassword, + $admin_resolveAbuseUserReport, + $admin_sendEmail, + $admin_serverInfo, + $admin_showModerationLogs, + $admin_showUser, + $admin_showUsers, + $admin_silenceUser, + $admin_suspendUser, + $admin_unsilenceUser, + $admin_unsuspendUser, + $admin_updateMeta, + $admin_deleteAccount, + $admin_updateUserNote, + $announcements, + $antennas_create, + $antennas_delete, + $antennas_list, + $antennas_notes, + $antennas_show, + $antennas_update, + $ap_get, + $ap_show, + $app_create, + $app_show, + $auth_accept, + $auth_session_generate, + $auth_session_show, + $auth_session_userkey, + $blocking_create, + $blocking_delete, + $blocking_list, + $channels_create, + $channels_featured, + $channels_follow, + $channels_followed, + $channels_owned, + $channels_show, + $channels_timeline, + $channels_unfollow, + $channels_update, + $charts_activeUsers, + $charts_apRequest, + $charts_drive, + $charts_federation, + $charts_hashtag, + $charts_instance, + $charts_notes, + $charts_user_drive, + $charts_user_following, + $charts_user_notes, + $charts_user_reactions, + $charts_users, + $clips_addNote, + $clips_removeNote, + $clips_create, + $clips_delete, + $clips_list, + $clips_notes, + $clips_show, + $clips_update, + $drive, + $drive_files, + $drive_files_attachedNotes, + $drive_files_checkExistence, + $drive_files_create, + $drive_files_delete, + $drive_files_findByHash, + $drive_files_find, + $drive_files_show, + $drive_files_update, + $drive_files_uploadFromUrl, + $drive_folders, + $drive_folders_create, + $drive_folders_delete, + $drive_folders_find, + $drive_folders_show, + $drive_folders_update, + $drive_stream, + $emailAddress_available, + $endpoint, + $endpoints, + $exportCustomEmojis, + $federation_followers, + $federation_following, + $federation_instances, + $federation_showInstance, + $federation_updateRemoteUser, + $federation_users, + $federation_stats, + $following_create, + $following_delete, + $following_invalidate, + $following_requests_accept, + $following_requests_cancel, + $following_requests_list, + $following_requests_reject, + $gallery_featured, + $gallery_popular, + $gallery_posts, + $gallery_posts_create, + $gallery_posts_delete, + $gallery_posts_like, + $gallery_posts_show, + $gallery_posts_unlike, + $gallery_posts_update, + $getOnlineUsersCount, + $hashtags_list, + $hashtags_search, + $hashtags_show, + $hashtags_trend, + $hashtags_users, + $i, + $i_2fa_done, + $i_2fa_keyDone, + $i_2fa_passwordLess, + $i_2fa_registerKey, + $i_2fa_register, + $i_2fa_removeKey, + $i_2fa_unregister, + $i_apps, + $i_authorizedApps, + $i_changePassword, + $i_deleteAccount, + $i_exportBlocking, + $i_exportFollowing, + $i_exportMute, + $i_exportNotes, + $i_exportUserLists, + $i_favorites, + $i_gallery_likes, + $i_gallery_posts, + $i_getWordMutedNotesCount, + $i_importBlocking, + $i_importFollowing, + $i_importMuting, + $i_importUserLists, + $i_notifications, + $i_pageLikes, + $i_pages, + $i_pin, + $i_readAllMessagingMessages, + $i_readAllUnreadNotes, + $i_readAnnouncement, + $i_regenerateToken, + $i_registry_getAll, + $i_registry_getDetail, + $i_registry_get, + $i_registry_keysWithType, + $i_registry_keys, + $i_registry_remove, + $i_registry_scopes, + $i_registry_set, + $i_revokeToken, + $i_signinHistory, + $i_unpin, + $i_updateEmail, + $i_update, + $i_userGroupInvites, + $i_webhooks_create, + $i_webhooks_list, + $i_webhooks_show, + $i_webhooks_update, + $i_webhooks_delete, + $messaging_history, + $messaging_messages, + $messaging_messages_create, + $messaging_messages_delete, + $messaging_messages_read, + $meta, + $miauth_genToken, + $mute_create, + $mute_delete, + $mute_list, + $my_apps, + $notes, + $notes_children, + $notes_clips, + $notes_conversation, + $notes_create, + $notes_delete, + $notes_favorites_create, + $notes_favorites_delete, + $notes_featured, + $notes_globalTimeline, + $notes_hybridTimeline, + $notes_localTimeline, + $notes_mentions, + $notes_polls_recommendation, + $notes_polls_vote, + $notes_reactions, + $notes_reactions_create, + $notes_reactions_delete, + $notes_renotes, + $notes_replies, + $notes_searchByTag, + $notes_search, + $notes_show, + $notes_state, + $notes_threadMuting_create, + $notes_threadMuting_delete, + $notes_timeline, + $notes_translate, + $notes_unrenote, + $notes_userListTimeline, + $notifications_create, + $notifications_markAllAsRead, + $notifications_read, + $pagePush, + $pages_create, + $pages_delete, + $pages_featured, + $pages_like, + $pages_show, + $pages_unlike, + $pages_update, + $ping, + $pinnedUsers, + $promo_read, + $requestResetPassword, + $resetDb, + $resetPassword, + $serverInfo, + $stats, + $sw_register, + $sw_unregister, + $test, + $username_available, + $users, + $users_clips, + $users_followers, + $users_following, + $users_gallery_posts, + $users_getFrequentlyRepliedUsers, + $users_groups_create, + $users_groups_delete, + $users_groups_invitations_accept, + $users_groups_invitations_reject, + $users_groups_invite, + $users_groups_joined, + $users_groups_leave, + $users_groups_owned, + $users_groups_pull, + $users_groups_show, + $users_groups_transfer, + $users_groups_update, + $users_lists_create, + $users_lists_delete, + $users_lists_list, + $users_lists_pull, + $users_lists_push, + $users_lists_show, + $users_lists_update, + $users_notes, + $users_pages, + $users_reactions, + $users_recommendation, + $users_relation, + $users_reportAbuse, + $users_searchByUsernameAndHost, + $users_search, + $users_show, + $users_stats, + $admin_driveCapOverride, + $fetchRss, + ], +}) +export class EndpointsModule {} diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts new file mode 100644 index 0000000000..d390a47b8f --- /dev/null +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -0,0 +1,89 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Limiter from 'ratelimiter'; +import Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; +import Logger from '@/logger.js'; +import type { IEndpointMeta } from './endpoints.js'; + +const logger = new Logger('limiter'); + +@Injectable() +export class RateLimiterService { + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + ) { + } + + public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) { + return new Promise((ok, reject) => { + if (process.env.NODE_ENV === 'test') ok(); + + // Short-term limit + const min = (): void => { + const minIntervalLimiter = new Limiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval, + max: 1, + db: this.redisClient, + }); + + minIntervalLimiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('BRIEF_REQUEST_INTERVAL'); + } else { + if (hasLongTermLimit) { + max(); + } else { + ok(); + } + } + }); + }; + + // Long term limit + const max = (): void => { + const limiter = new Limiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration, + max: limitation.max, + db: this.redisClient, + }); + + limiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('RATE_LIMIT_EXCEEDED'); + } else { + ok(); + } + }); + }; + + const hasShortTermLimit = typeof limitation.minInterval === 'number'; + + const hasLongTermLimit = + typeof limitation.duration === 'number' && + typeof limitation.max === 'number'; + + if (hasShortTermLimit) { + min(); + } else if (hasLongTermLimit) { + max(); + } else { + ok(); + } + }); + } +} diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts new file mode 100644 index 0000000000..5cda3c6205 --- /dev/null +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -0,0 +1,282 @@ +import { randomBytes } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { RateLimiterService } from './RateLimiterService.js'; +import { SigninService } from './SigninService.js'; +import type Koa from 'koa'; + +@Injectable() +export class SigninApiService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, + + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private idService: IdService, + private rateLimiterService: RateLimiterService, + private signinService: SigninService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, + ) { + } + + public async signin(ctx: Koa.Context) { + ctx.set('Access-Control-Allow-Origin', this.config.url); + ctx.set('Access-Control-Allow-Credentials', 'true'); + + const body = ctx.request.body as any; + const username = body['username']; + const password = body['password']; + const token = body['token']; + + function error(status: number, error: { id: string }) { + ctx.status = status; + ctx.body = { error }; + } + + try { + // not more than 1 attempt per second and not more than 10 attempts per hour + await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); + } catch (err) { + ctx.status = 429; + ctx.body = { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + return; + } + + if (typeof username !== 'string') { + ctx.status = 400; + return; + } + + if (typeof password !== 'string') { + ctx.status = 400; + return; + } + + if (token != null && typeof token !== 'string') { + ctx.status = 400; + return; + } + + // Fetch user + const user = await this.usersRepository.findOneBy({ + usernameLower: username.toLowerCase(), + host: IsNull(), + }) as ILocalUser; + + if (user == null) { + error(404, { + id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', + }); + return; + } + + if (user.isSuspended) { + error(403, { + id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', + }); + return; + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + // Compare password + const same = await bcrypt.compare(password, profile.password!); + + const fail = async (status?: number, failure?: { id: string }) => { + // Append signin history + await this.signinsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: user.id, + ip: ctx.ip, + headers: ctx.headers, + success: false, + }); + + error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); + }; + + if (!profile.twoFactorEnabled) { + if (same) { + this.signinService.signin(ctx, user); + return; + } else { + await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + return; + } + } + + if (token) { + if (!same) { + await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + return; + } + + const verified = (speakeasy as any).totp.verify({ + secret: profile.twoFactorSecret, + encoding: 'base32', + token: token, + window: 2, + }); + + if (verified) { + this.signinService.signin(ctx, user); + return; + } else { + await fail(403, { + id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', + }); + return; + } + } else if (body.credentialId) { + if (!same && !profile.usePasswordLessLogin) { + await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + return; + } + + const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); + const clientData = JSON.parse(clientDataJSON.toString('utf-8')); + const challenge = await this.attestationChallengesRepository.findOneBy({ + userId: user.id, + id: body.challengeId, + registrationChallenge: false, + challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), + }); + + if (!challenge) { + await fail(403, { + id: '2715a88a-2125-4013-932f-aa6fe72792da', + }); + return; + } + + await this.attestationChallengesRepository.delete({ + userId: user.id, + id: body.challengeId, + }); + + if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { + await fail(403, { + id: '2715a88a-2125-4013-932f-aa6fe72792da', + }); + return; + } + + const securityKey = await this.userSecurityKeysRepository.findOneBy({ + id: Buffer.from( + body.credentialId + .replace(/-/g, '+') + .replace(/_/g, '/'), + 'base64', + ).toString('hex'), + }); + + if (!securityKey) { + await fail(403, { + id: '66269679-aeaf-4474-862b-eb761197e046', + }); + return; + } + + const isValid = this.twoFactorAuthenticationService.verifySignin({ + publicKey: Buffer.from(securityKey.publicKey, 'hex'), + authenticatorData: Buffer.from(body.authenticatorData, 'hex'), + clientDataJSON, + clientData, + signature: Buffer.from(body.signature, 'hex'), + challenge: challenge.challenge, + }); + + if (isValid) { + this.signinService.signin(ctx, user); + return; + } else { + await fail(403, { + id: '93b86c4b-72f9-40eb-9815-798928603d1e', + }); + return; + } + } else { + if (!same && !profile.usePasswordLessLogin) { + await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + return; + } + + const keys = await this.userSecurityKeysRepository.findBy({ + userId: user.id, + }); + + if (keys.length === 0) { + await fail(403, { + id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4', + }); + return; + } + + // 32 byte challenge + const challenge = randomBytes(32).toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const challengeId = this.idService.genId(); + + await this.attestationChallengesRepository.insert({ + userId: user.id, + id: challengeId, + challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), + createdAt: new Date(), + registrationChallenge: false, + }); + + ctx.body = { + challenge, + challengeId, + securityKeys: keys.map(key => ({ + id: key.id, + })), + }; + ctx.status = 200; + return; + } + // never get here + } +} + diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts new file mode 100644 index 0000000000..19d14bad67 --- /dev/null +++ b/packages/backend/src/server/api/SigninService.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { SigninsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +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 type Koa from 'koa'; + +@Injectable() +export class SigninService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private signinEntityService: SigninEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + } + + public signin(ctx: Koa.Context, user: ILocalUser, redirect = false) { + if (redirect) { + //#region Cookie + ctx.cookies.set('igi', user.token!, { + path: '/', + // SEE: https://github.com/koajs/koa/issues/974 + // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header + secure: this.config.url.startsWith('https'), + httpOnly: false, + }); + //#endregion + + ctx.redirect(this.config.url); + } else { + ctx.body = { + id: user.id, + i: user.token, + }; + ctx.status = 200; + } + + (async () => { + // Append signin history + const record = await this.signinsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: user.id, + ip: ctx.ip, + headers: ctx.headers, + success: true, + }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish signin event + this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); + })(); + } +} + diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts new file mode 100644 index 0000000000..df040ddcf8 --- /dev/null +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -0,0 +1,175 @@ +import { Inject, Injectable } from '@nestjs/common'; +import rndstr from 'rndstr'; +import bcrypt from 'bcryptjs'; +import { DI } from '@/di-symbols.js'; +import { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { MetaService } from '@/core/MetaService.js'; +import { CaptchaService } from '@/core/CaptchaService.js'; +import { IdService } from '@/core/IdService.js'; +import { SignupService } from '@/core/SignupService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { SigninService } from './SigninService.js'; +import type Koa from 'koa'; + +@Injectable() +export class SignupApiService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.userPendingsRepository) + private userPendingsRepository: UserPendingsRepository, + + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private metaService: MetaService, + private captchaService: CaptchaService, + private signupService: SignupService, + private signinService: SigninService, + private emailService: EmailService, + ) { + } + + public async signup(ctx: Koa.Context) { + const body = ctx.request.body; + + const instance = await this.metaService.fetch(true); + + // Verify *Captcha + // ただしテスト時はこの機構は障害となるため無効にする + if (process.env.NODE_ENV !== 'test') { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + } + + const username = body['username']; + const password = body['password']; + const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null; + const invitationCode = body['invitationCode']; + const emailAddress = body['emailAddress']; + + if (instance.emailRequiredForSignup) { + if (emailAddress == null || typeof emailAddress !== 'string') { + ctx.status = 400; + return; + } + + const available = await this.emailService.validateEmailForAccount(emailAddress); + if (!available) { + ctx.status = 400; + return; + } + } + + if (instance.disableRegistration) { + if (invitationCode == null || typeof invitationCode !== 'string') { + ctx.status = 400; + return; + } + + const ticket = await this.registrationTicketsRepository.findOneBy({ + code: invitationCode, + }); + + if (ticket == null) { + ctx.status = 400; + return; + } + + this.registrationTicketsRepository.delete(ticket.id); + } + + if (instance.emailRequiredForSignup) { + const code = rndstr('a-z0-9', 16); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + await this.userPendingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + code, + email: emailAddress, + username: username, + password: hash, + }); + + const link = `${this.config.url}/signup-complete/${code}`; + + sendEmail(emailAddress, 'Signup', + `To complete signup, please click this link:
${link}`, + `To complete signup, please click this link: ${link}`); + + ctx.status = 204; + } else { + try { + const { account, secret } = await this.signupService.signup({ + username, password, host, + }); + + const res = await this.userEntityService.pack(account, account, { + detail: true, + includeSecrets: true, + }); + + (res as any).token = secret; + + ctx.body = res; + } catch (e) { + ctx.throw(400, e); + } + } + } + + public async signupPending(ctx: Koa.Context) { + const body = ctx.request.body; + + const code = body['code']; + + try { + const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); + + const { account, secret } = await this.signupService.signup({ + username: pendingUser.username, + passwordHash: pendingUser.password, + }); + + this.userPendingsRepository.delete({ + id: pendingUser.id, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: account.id }); + + await this.userProfilesRepository.update({ userId: profile.userId }, { + email: pendingUser.email, + emailVerified: true, + emailVerifyCode: null, + }); + + this.signinService.signin(ctx, account); + } catch (e) { + ctx.throw(400, e); + } + } +} diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts new file mode 100644 index 0000000000..b08b01aef9 --- /dev/null +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -0,0 +1,120 @@ +import { EventEmitter } from 'events'; +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import * as websocket from 'websocket'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { AuthenticateService } from './AuthenticateService.js'; +import MainStreamConnection from './stream/index.js'; +import { ChannelsService } from './stream/ChannelsService.js'; +import type { ParsedUrlQuery } from 'querystring'; +import type * as http from 'node:http'; + +@Injectable() +export class StreamingApiServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private globalEventService: GlobalEventService, + private noteReadService: NoteReadService, + private authenticateService: AuthenticateService, + private channelsService: ChannelsService, + private notificationService: NotificationService, + ) { + } + + public attachStreamingApi(server: http.Server) { + // Init websocket server + const ws = new websocket.server({ + httpServer: server, + }); + + ws.on('request', async (request) => { + const q = request.resourceURL.query as ParsedUrlQuery; + + // TODO: トークンが間違ってるなどしてauthenticateに失敗したら + // コネクション切断するなりエラーメッセージ返すなりする + // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので) + const [user, miapp] = await this.authenticateService.authenticate(q.i as string); + + if (user?.isSuspended) { + request.reject(400); + return; + } + + const connection = request.accept(); + + const ev = new EventEmitter(); + + async function onRedisMessage(_: string, data: string) { + const parsed = JSON.parse(data); + ev.emit(parsed.channel, parsed.message); + } + + this.redisSubscriber.on('message', onRedisMessage); + + const main = new MainStreamConnection( + this.followingsRepository, + this.mutingsRepository, + this.blockingsRepository, + this.channelFollowingsRepository, + this.userProfilesRepository, + this.channelsService, + this.globalEventService, + this.noteReadService, + this.notificationService, + connection, ev, user, miapp, + ); + + const intervalId = user ? setInterval(() => { + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); + }, 1000 * 60 * 5) : null; + if (user) { + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); + } + + connection.once('close', () => { + ev.removeAllListeners(); + main.dispose(); + this.redisSubscriber.off('message', onRedisMessage); + if (intervalId) clearInterval(intervalId); + }); + + connection.on('message', async (data) => { + if (data.type === 'utf8' && data.utf8Data === 'ping') { + connection.send('pong'); + } + }); + }); + } +} diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts deleted file mode 100644 index ec71ddd2c0..0000000000 --- a/packages/backend/src/server/api/api-handler.ts +++ /dev/null @@ -1,92 +0,0 @@ -import Koa from 'koa'; - -import { User } from '@/models/entities/user.js'; -import { UserIps } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { IEndpoint } from './endpoints.js'; -import authenticate, { AuthenticationError } from './authenticate.js'; -import call from './call.js'; -import { ApiError } from './error.js'; - -const userIpHistories = new Map>(); - -setInterval(() => { - userIpHistories.clear(); -}, 1000 * 60 * 60); - -export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { - const body = ctx.is('multipart/form-data') - ? (ctx.request as any).body - : ctx.method === 'GET' - ? ctx.query - : ctx.request.body; - - const reply = (x?: any, y?: ApiError) => { - if (x == null) { - ctx.status = 204; - } else if (typeof x === 'number' && y) { - ctx.status = x; - ctx.body = { - error: { - message: y!.message, - code: y!.code, - id: y!.id, - kind: y!.kind, - ...(y!.info ? { info: y!.info } : {}), - }, - }; - } else { - // 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない - ctx.body = typeof x === 'string' ? JSON.stringify(x) : x; - } - res(); - }; - - // Authentication - authenticate(body['i']).then(([user, app]) => { - // API invoking - call(endpoint.name, user, app, body, ctx).then((res: any) => { - if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { - ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); - } - reply(res); - }).catch((e: ApiError) => { - reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); - }); - - // Log IP - if (user) { - fetchMeta().then(meta => { - if (!meta.enableIpLogging) return; - const ip = ctx.ip; - const ips = userIpHistories.get(user.id); - if (ips == null || !ips.has(ip)) { - if (ips == null) { - userIpHistories.set(user.id, new Set([ip])); - } else { - ips.add(ip); - } - - try { - UserIps.createQueryBuilder().insert().values({ - createdAt: new Date(), - userId: user.id, - ip: ip, - }).orIgnore(true).execute(); - } catch { - } - } - }); - } - }).catch(e => { - if (e instanceof AuthenticationError) { - reply(403, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - reply(500, new ApiError()); - } - }); -}); diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts deleted file mode 100644 index 65ccfcf551..0000000000 --- a/packages/backend/src/server/api/authenticate.ts +++ /dev/null @@ -1,66 +0,0 @@ -import isNativeToken from './common/is-native-token.js'; -import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; -import { Users, AccessTokens, Apps } from '@/models/index.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { Cache } from '@/misc/cache.js'; -import { App } from '@/models/entities/app.js'; -import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; - -const appCache = new Cache(Infinity); - -export class AuthenticationError extends Error { - constructor(message: string) { - super(message); - this.name = 'AuthenticationError'; - } -} - -export default async (token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { - if (token == null) { - return [null, null]; - } - - if (isNativeToken(token)) { - const user = await localUserByNativeTokenCache.fetch(token, - () => Users.findOneBy({ token }) as Promise); - - if (user == null) { - throw new AuthenticationError('user not found'); - } - - return [user, null]; - } else { - const accessToken = await AccessTokens.findOne({ - where: [{ - hash: token.toLowerCase(), // app - }, { - token: token, // miauth - }], - }); - - if (accessToken == null) { - throw new AuthenticationError('invalid signature'); - } - - AccessTokens.update(accessToken.id, { - lastUsedAt: new Date(), - }); - - const user = await localUserByIdCache.fetch(accessToken.userId, - () => Users.findOneBy({ - id: accessToken.userId, - }) as Promise); - - if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, - () => Apps.findOneByOrFail({ id: accessToken.appId! })); - - return [user, { - id: accessToken.id, - permission: app.permission, - } as AccessToken]; - } else { - return [user, accessToken]; - } - } -}; diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts deleted file mode 100644 index aa130459a3..0000000000 --- a/packages/backend/src/server/api/call.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { performance } from 'perf_hooks'; -import Koa from 'koa'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; -import { limiter } from './limiter.js'; -import endpoints, { IEndpointMeta } from './endpoints.js'; -import { ApiError } from './error.js'; -import { apiLogger } from './logger.js'; - -const accessDenied = { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', -}; - -export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { - const isSecure = user != null && token == null; - const isModerator = user != null && (user.isModerator || user.isAdmin); - - const ep = endpoints.find(e => e.name === endpoint); - - if (ep == null) { - throw new ApiError({ - message: 'No such endpoint.', - code: 'NO_SUCH_ENDPOINT', - id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709', - httpStatusCode: 404, - }); - } - - if (ep.meta.secure && !isSecure) { - throw new ApiError(accessDenied); - } - - if (ep.meta.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. - let limitActor: string; - if (user) { - limitActor = user.id; - } else { - limitActor = getIpHash(ctx!.ip); - } - - const limit = Object.assign({}, ep.meta.limit); - - if (!limit.key) { - limit.key = ep.name; - } - - // Rate limit - await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(e => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }); - }); - } - - 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.requireModerator && !isModerator) { - throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); - } - - if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { - throw new ApiError({ - message: 'Your app does not have the necessary permissions to use this endpoint.', - code: 'PERMISSION_DENIED', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); - } - - // Cast non JSON input - if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) { - for (const k of Object.keys(ep.params.properties)) { - const param = ep.params.properties![k]; - if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { - try { - data[k] = JSON.parse(data[k]); - } catch (e) { - throw new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', - }, { - param: k, - reason: `cannot cast to ${param.type}`, - }); - } - } - } - } - - // API invoking - const before = performance.now(); - return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => { - if (e instanceof ApiError) { - throw e; - } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { - ep: ep.name, - ps: data, - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); - throw new ApiError(null, { - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); - } - }).finally(() => { - const after = performance.now(); - const time = after - before; - if (time > 1000) { - apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`); - } - }); -}; diff --git a/packages/backend/src/server/api/common/GetterService.ts b/packages/backend/src/server/api/common/GetterService.ts new file mode 100644 index 0000000000..a6b60d1f5a --- /dev/null +++ b/packages/backend/src/server/api/common/GetterService.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; + +@Injectable() +export class GetterService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + ) { + } + + /** + * Get note for API processing + */ + public async getNote(noteId: Note['id']) { + const note = await this.notesRepository.findOneBy({ id: noteId }); + + if (note == null) { + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + } + + return note; + } + + /** + * Get user for API processing + */ + public async getUser(userId: User['id']) { + const user = await this.usersRepository.findOneBy({ id: userId }); + + if (user == null) { + throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); + } + + return user; + } + + /** + * Get remote user for API processing + */ + public async getRemoteUser(userId: User['id']) { + const user = await this.getUser(userId); + + if (!this.userEntityService.isRemoteUser(user)) { + throw new Error('user is not a remote user'); + } + + return user; + } + + /** + * Get local user for API processing + */ + public async getLocalUser(userId: User['id']) { + const user = await this.getUser(userId); + + if (!this.userEntityService.isLocalUser(user)) { + throw new Error('user is not a local user'); + } + + return user; + } +} + diff --git a/packages/backend/src/server/api/common/generate-block-query.ts b/packages/backend/src/server/api/common/generate-block-query.ts deleted file mode 100644 index 60db1e731b..0000000000 --- a/packages/backend/src/server/api/common/generate-block-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Blockings } from '@/models/index.js'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -// ここでいうBlockedは被Blockedの意 -export function generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }) { - const blockingQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockerId') - .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); - - // 投稿の作者にブロックされていない かつ - // 投稿の返信先の作者にブロックされていない かつ - // 投稿の引用元の作者にブロックされていない - q - .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where(`note.replyUserId IS NULL`) - .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { qb - .where(`note.renoteUserId IS NULL`) - .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); - })); - - q.setParameters(blockingQuery.getParameters()); -} - -export function generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }) { - const blockingQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockeeId') - .where('blocking.blockerId = :blockerId', { blockerId: me.id }); - - const blockedQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockerId') - .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); - - q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`); - q.setParameters(blockingQuery.getParameters()); - - q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`); - q.setParameters(blockedQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/generate-channel-query.ts b/packages/backend/src/server/api/common/generate-channel-query.ts deleted file mode 100644 index 333bb73b86..0000000000 --- a/packages/backend/src/server/api/common/generate-channel-query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { ChannelFollowings } from '@/models/index.js'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateChannelQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null) { - if (me == null) { - q.andWhere('note.channelId IS NULL'); - } else { - q.leftJoinAndSelect('note.channel', 'channel'); - - const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing') - .select('channelFollowing.followeeId') - .where('channelFollowing.followerId = :followerId', { followerId: me.id }); - - q.andWhere(new Brackets(qb => { qb - // チャンネルのノートではない - .where('note.channelId IS NULL') - // または自分がフォローしているチャンネルのノート - .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); - })); - - q.setParameters(channelFollowingQuery.getParameters()); - } -} diff --git a/packages/backend/src/server/api/common/generate-muted-note-query.ts b/packages/backend/src/server/api/common/generate-muted-note-query.ts deleted file mode 100644 index f544e334d3..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-query.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { MutedNotes } from '@/models/index.js'; -import { SelectQueryBuilder } from 'typeorm'; - -export function generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: User['id'] }) { - const mutedQuery = MutedNotes.createQueryBuilder('muted') - .select('muted.noteId') - .where('muted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - - q.setParameters(mutedQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts b/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts deleted file mode 100644 index 7263ea2e60..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { NoteThreadMutings } from '@/models/index.js'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }) { - const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted') - .select('threadMuted.threadId') - .where('threadMuted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - q.andWhere(new Brackets(qb => { qb - .where(`note.threadId IS NULL`) - .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); - })); - - q.setParameters(mutedQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/generate-muted-user-query.ts b/packages/backend/src/server/api/common/generate-muted-user-query.ts deleted file mode 100644 index 470ece1a62..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-user-query.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { SelectQueryBuilder, Brackets } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { Mutings, UserProfiles } from '@/models/index.js'; - -export function generateMutedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }, exclude?: User) { - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - if (exclude) { - mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); - } - - const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile') - .select('user_profile.mutedInstances') - .where('user_profile.userId = :muterId', { muterId: me.id }); - - // 投稿の作者をミュートしていない かつ - // 投稿の返信先の作者をミュートしていない かつ - // 投稿の引用元の作者をミュートしていない - q - .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('note.replyUserId IS NULL') - .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { qb - .where('note.renoteUserId IS NULL') - .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); - })) - // mute instances - .andWhere(new Brackets(qb => { qb - .andWhere('note.userHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); - })) - .andWhere(new Brackets(qb => { qb - .where('note.replyUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); - })) - .andWhere(new Brackets(qb => { qb - .where('note.renoteUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); - })); - - q.setParameters(mutingQuery.getParameters()); - q.setParameters(mutingInstanceQuery.getParameters()); -} - -export function generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }) { - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); - - q.setParameters(mutingQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/generate-native-user-token.ts b/packages/backend/src/server/api/common/generate-native-user-token.ts deleted file mode 100644 index 5d8a4c5378..0000000000 --- a/packages/backend/src/server/api/common/generate-native-user-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { secureRndstr } from '@/misc/secure-rndstr.js'; - -export default () => secureRndstr(16, true); diff --git a/packages/backend/src/server/api/common/generate-replies-query.ts b/packages/backend/src/server/api/common/generate-replies-query.ts deleted file mode 100644 index 301782eab9..0000000000 --- a/packages/backend/src/server/api/common/generate-replies-query.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateRepliesQuery(q: SelectQueryBuilder, me?: Pick | null) { - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where(`note.replyId IS NULL`) // 返信ではない - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.replyUserId = note.userId'); - })); - })); - } else if (!me.showTimelineReplies) { - q.andWhere(new Brackets(qb => { qb - .where(`note.replyId IS NULL`) // 返信ではない - .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 - .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.userId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.replyUserId = note.userId'); - })); - })); - } -} diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts deleted file mode 100644 index b50b6812f4..0000000000 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Followings } from '@/models/index.js'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null) { - // This code must always be synchronized with the checks in Notes.isVisibleForMe. - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })); - } else { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :meId'); - - q.andWhere(new Brackets(qb => { qb - // 公開投稿である - .where(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })) - // または 自分自身 - .orWhere('note.userId = :meId') - // または 自分宛て - .orWhere(':meId = ANY(note.visibleUserIds)') - .orWhere(':meId = ANY(note.mentions)') - .orWhere(new Brackets(qb => { qb - // または フォロワー宛ての投稿であり、 - .where(`note.visibility = 'followers'`) - .andWhere(new Brackets(qb => { qb - // 自分がフォロワーである - .where(`note.userId IN (${ followingQuery.getQuery() })`) - // または 自分の投稿へのリプライ - .orWhere('note.replyUserId = :meId'); - })); - })); - })); - - q.setParameters({ meId: me.id }); - } -} diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts deleted file mode 100644 index 783ea9ef75..0000000000 --- a/packages/backend/src/server/api/common/getters.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes, Users } from '@/models/index.js'; - -/** - * Get note for API processing - */ -export async function getNote(noteId: Note['id']) { - const note = await Notes.findOneBy({ id: noteId }); - - if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); - } - - return note; -} - -/** - * Get user for API processing - */ -export async function getUser(userId: User['id']) { - const user = await Users.findOneBy({ id: userId }); - - if (user == null) { - throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); - } - - return user; -} - -/** - * Get remote user for API processing - */ -export async function getRemoteUser(userId: User['id']) { - const user = await getUser(userId); - - if (!Users.isRemoteUser(user)) { - throw new Error('user is not a remote user'); - } - - return user; -} - -/** - * Get local user for API processing - */ -export async function getLocalUser(userId: User['id']) { - const user = await getUser(userId); - - if (!Users.isLocalUser(user)) { - throw new Error('user is not a local user'); - } - - return user; -} diff --git a/packages/backend/src/server/api/common/inject-featured.ts b/packages/backend/src/server/api/common/inject-featured.ts index f7cdd365ed..75126fa304 100644 --- a/packages/backend/src/server/api/common/inject-featured.ts +++ b/packages/backend/src/server/api/common/inject-featured.ts @@ -1,6 +1,6 @@ import rndstr from 'rndstr'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; +import { Note } from '@/models/entities/Note.js'; +import { User } from '@/models/entities/User.js'; import { Notes, UserProfiles, NoteReactions } from '@/models/index.js'; import { generateMutedUserQuery } from './generate-muted-user-query.js'; import { generateBlockedUserQuery } from './generate-block-query.js'; diff --git a/packages/backend/src/server/api/common/inject-promo.ts b/packages/backend/src/server/api/common/inject-promo.ts index b0da8118b4..454f5dbb0e 100644 --- a/packages/backend/src/server/api/common/inject-promo.ts +++ b/packages/backend/src/server/api/common/inject-promo.ts @@ -1,6 +1,6 @@ import rndstr from 'rndstr'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; +import { Note } from '@/models/entities/Note.js'; +import { User } from '@/models/entities/User.js'; import { PromoReads, PromoNotes, Notes, Users } from '@/models/index.js'; export async function injectPromo(timeline: Note[], user?: User | null) { diff --git a/packages/backend/src/server/api/common/is-native-token.ts b/packages/backend/src/server/api/common/is-native-token.ts deleted file mode 100644 index 2833c570c8..0000000000 --- a/packages/backend/src/server/api/common/is-native-token.ts +++ /dev/null @@ -1 +0,0 @@ -export default (token: string) => token.length === 16; diff --git a/packages/backend/src/server/api/common/make-pagination-query.ts b/packages/backend/src/server/api/common/make-pagination-query.ts deleted file mode 100644 index 51c11e5dff..0000000000 --- a/packages/backend/src/server/api/common/make-pagination-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SelectQueryBuilder } from 'typeorm'; - -export function makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) { - if (sinceId && untilId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); - } else if (sinceId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.orderBy(`${q.alias}.id`, 'ASC'); - } else if (untilId) { - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); - } else if (sinceDate && untilDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); - } else if (sinceDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.orderBy(`${q.alias}.createdAt`, 'ASC'); - } else if (untilDate) { - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); - } else { - q.orderBy(`${q.alias}.id`, 'DESC'); - } - return q; -} diff --git a/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts deleted file mode 100644 index c4c18ffa06..0000000000 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; -import { publishMessagingStream } from '@/services/stream.js'; -import { publishMessagingIndexStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; -import { In } from 'typeorm'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { toArray } from '@/prelude/array.js'; -import { renderReadActivity } from '@/remote/activitypub/renderer/read.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { deliver } from '@/queue/index.js'; -import orderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; - -/** - * Mark messages as read - */ -export async function readUserMessagingMessage( - userId: User['id'], - otherpartyId: User['id'], - messageIds: MessagingMessage['id'][] -) { - if (messageIds.length === 0) return; - - const messages = await MessagingMessages.findBy({ - id: In(messageIds), - }); - - for (const message of messages) { - if (message.recipientId !== userId) { - throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); - } - } - - // Update documents - await MessagingMessages.update({ - id: In(messageIds), - userId: otherpartyId, - recipientId: userId, - isRead: false, - }, { - isRead: true, - }); - - // Publish event - publishMessagingStream(otherpartyId, userId, 'read', messageIds); - publishMessagingIndexStream(userId, 'read', messageIds); - - if (!await Users.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllMessagingMessages'); - pushNotification(userId, 'readAllMessagingMessages', undefined); - } else { - // そのユーザーとのメッセージで未読がなければイベント発行 - const count = await MessagingMessages.count({ - where: { - userId: otherpartyId, - recipientId: userId, - isRead: false, - }, - take: 1 - }); - - if (!count) { - pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); - } - } -} - -/** - * Mark messages as read - */ -export async function readGroupMessagingMessage( - userId: User['id'], - groupId: UserGroup['id'], - messageIds: MessagingMessage['id'][] -) { - if (messageIds.length === 0) return; - - // check joined - const joining = await UserGroupJoinings.findOneBy({ - userId: userId, - userGroupId: groupId, - }); - - if (joining == null) { - throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); - } - - const messages = await MessagingMessages.findBy({ - id: In(messageIds), - }); - - const reads: MessagingMessage['id'][] = []; - - for (const message of messages) { - if (message.userId === userId) continue; - if (message.reads.includes(userId)) continue; - - // Update document - await MessagingMessages.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${joining.userId}')`) as any, - }) - .where('id = :id', { id: message.id }) - .execute(); - - reads.push(message.id); - } - - // Publish event - publishGroupMessagingStream(groupId, 'read', { - ids: reads, - userId: userId, - }); - publishMessagingIndexStream(userId, 'read', reads); - - if (!await Users.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllMessagingMessages'); - pushNotification(userId, 'readAllMessagingMessages', undefined); - } else { - // そのグループにおいて未読がなければイベント発行 - const unreadExist = await MessagingMessages.createQueryBuilder('message') - .where(`message.groupId = :groupId`, { groupId: groupId }) - .andWhere('message.userId != :userId', { userId: userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) - .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない - .getOne().then(x => x != null); - - if (!unreadExist) { - pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); - } - } -} - -export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) { - messages = toArray(messages).filter(x => x.uri); - const contents = messages.map(x => renderReadActivity(user, x)); - - if (contents.length > 1) { - const collection = orderedCollection(null, contents.length, undefined, undefined, contents); - deliver(user, renderActivity(collection), recipient.inbox); - } else { - for (const content of contents) { - deliver(user, renderActivity(content), recipient.inbox); - } - } -} diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts deleted file mode 100644 index b0d38a9e39..0000000000 --- a/packages/backend/src/server/api/common/read-notification.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { In } from 'typeorm'; -import { publishMainStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { User } from '@/models/entities/user.js'; -import { Notification } from '@/models/entities/notification.js'; -import { Notifications, Users } from '@/models/index.js'; - -export async function readNotification( - userId: User['id'], - notificationIds: Notification['id'][], -) { - if (notificationIds.length === 0) return; - - // Update documents - const result = await Notifications.update({ - notifieeId: userId, - id: In(notificationIds), - isRead: false, - }, { - isRead: true, - }); - - if (result.affected === 0) return; - - if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId); - else return postReadNotifications(userId, notificationIds); -} - -export async function readNotificationByQuery( - userId: User['id'], - query: Record, -) { - const notificationIds = await Notifications.findBy({ - ...query, - notifieeId: userId, - isRead: false, - }).then(notifications => notifications.map(notification => notification.id)); - - return readNotification(userId, notificationIds); -} - -function postReadAllNotifications(userId: User['id']) { - publishMainStream(userId, 'readAllNotifications'); - return pushNotification(userId, 'readAllNotifications', undefined); -} - -function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { - publishMainStream(userId, 'readNotifications', notificationIds); - return pushNotification(userId, 'readNotifications', { notificationIds }); -} diff --git a/packages/backend/src/server/api/common/signin.ts b/packages/backend/src/server/api/common/signin.ts deleted file mode 100644 index 038fd8d96e..0000000000 --- a/packages/backend/src/server/api/common/signin.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Koa from 'koa'; - -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { Signins } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { publishMainStream } from '@/services/stream.js'; - -export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { - if (redirect) { - //#region Cookie - ctx.cookies.set('igi', user.token!, { - path: '/', - // SEE: https://github.com/koajs/koa/issues/974 - // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header - secure: config.url.startsWith('https'), - httpOnly: false, - }); - //#endregion - - ctx.redirect(config.url); - } else { - ctx.body = { - id: user.id, - i: user.token, - }; - ctx.status = 200; - } - - (async () => { - // Append signin history - const record = await Signins.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: true, - }).then(x => Signins.findOneByOrFail(x.identifiers[0])); - - // Publish signin event - publishMainStream(user.id, 'signin', await Signins.pack(record)); - })(); -} diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts deleted file mode 100644 index abc142472a..0000000000 --- a/packages/backend/src/server/api/common/signup.ts +++ /dev/null @@ -1,114 +0,0 @@ -import bcrypt from 'bcryptjs'; -import { generateKeyPair } from 'node:crypto'; -import generateUserToken from './generate-native-user-token.js'; -import { User } from '@/models/entities/user.js'; -import { Users, UsedUsernames } from '@/models/index.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { IsNull } from 'typeorm'; -import { genId } from '@/misc/gen-id.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { usersChart } from '@/services/chart/index.js'; -import { UsedUsername } from '@/models/entities/used-username.js'; -import { db } from '@/db/postgre.js'; - -export async function signup(opts: { - username: User['username']; - password?: string | null; - passwordHash?: UserProfile['password'] | null; - host?: string | null; -}) { - const { username, password, passwordHash, host } = opts; - let hash = passwordHash; - - // Validate username - if (!Users.validateLocalUsername(username)) { - throw new Error('INVALID_USERNAME'); - } - - if (password != null && passwordHash == null) { - // Validate password - if (!Users.validatePassword(password)) { - throw new Error('INVALID_PASSWORD'); - } - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - hash = await bcrypt.hash(password, salt); - } - - // Generate secret - const secret = generateUserToken(); - - // Check username duplication - if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { - throw new Error('DUPLICATED_USERNAME'); - } - - // Check deleted username duplication - if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) { - throw new Error('USED_USERNAME'); - } - - const keyPair = await new Promise((res, rej) => - generateKeyPair('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined, - }, - } as any, (err, publicKey, privateKey) => - err ? rej(err) : res([publicKey, privateKey]) - )); - - let account!: User; - - // Start transaction - await db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - if (exist) throw new Error(' the username is already used'); - - account = await transactionalEntityManager.save(new User({ - id: genId(), - createdAt: new Date(), - username: username, - usernameLower: username.toLowerCase(), - host: toPunyNullable(host), - token: secret, - isAdmin: (await Users.countBy({ - host: IsNull(), - })) === 0, - })); - - await transactionalEntityManager.save(new UserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id, - })); - - await transactionalEntityManager.save(new UserProfile({ - userId: account.id, - autoAcceptFollowed: true, - password: hash, - })); - - await transactionalEntityManager.save(new UsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - })); - }); - - usersChart.update(account, true); - - return { account, secret }; -} diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts deleted file mode 100644 index c1b56b8a83..0000000000 --- a/packages/backend/src/server/api/define.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as fs from 'node:fs'; -import Ajv from 'ajv'; -import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; -import { Schema, SchemaType } from '@/misc/schema.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { IEndpointMeta } from './endpoints.js'; -import { ApiError } from './error.js'; - -export type Response = Record | void; - -// TODO: paramsの型をT['params']のスキーマ定義から推論する -type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record | null) => - Promise>>; - -const ajv = new Ajv({ - useDefaults: true, -}); - -ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); - -export default function (meta: T, paramDef: Ps, cb: executor) - : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record | null) => Promise { - const validate = ajv.compile(paramDef); - - return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record | null) => { - let cleanup: undefined | (() => void) = undefined; - - if (meta.requireFile) { - cleanup = () => { - fs.unlink(file.path, () => {}); - }; - - if (file == null) return Promise.reject(new ApiError({ - message: 'File required.', - code: 'FILE_REQUIRED', - id: '4267801e-70d1-416a-b011-4ee502885d8b', - })); - } - - const valid = validate(params); - if (!valid) { - if (file) cleanup!(); - - const errors = validate.errors!; - const err = new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - }, { - param: errors[0].schemaPath, - reason: errors[0].message, - }); - return Promise.reject(err); - } - - return cb(params as SchemaType, user, token, file, cleanup, ip, headers); - }; -} diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts new file mode 100644 index 0000000000..0a7f9b3008 --- /dev/null +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -0,0 +1,62 @@ +import * as fs from 'node:fs'; +import Ajv from 'ajv'; +import type { Schema, SchemaType } from '@/misc/schema.js'; +import type { CacheableLocalUser } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; +import { ApiError } from './error.js'; +import type { IEndpointMeta } from './endpoints.js'; + +const ajv = new Ajv({ + useDefaults: true, +}); + +ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + +export type Response = Record | void; + +// TODO: paramsの型をT['params']のスキーマ定義から推論する +type executor = + (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record | null) => + Promise>>; + +export abstract class Endpoint { + public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record | null) => Promise; + + constructor(meta: T, paramDef: Ps, cb: executor) { + const validate = ajv.compile(paramDef); + + this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record | null) => { + let cleanup: undefined | (() => void) = undefined; + + if (meta.requireFile) { + cleanup = () => { + fs.unlink(file.path, () => {}); + }; + + if (file == null) return Promise.reject(new ApiError({ + message: 'File required.', + code: 'FILE_REQUIRED', + id: '4267801e-70d1-416a-b011-4ee502885d8b', + })); + } + + const valid = validate(params); + if (!valid) { + if (file) cleanup!(); + + const errors = validate.errors!; + const err = new ApiError({ + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + }, { + param: errors[0].schemaPath, + reason: errors[0].message, + }); + return Promise.reject(err); + } + + return cb(params as SchemaType, user, token, file, cleanup, ip, headers); + }; + } +} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 4644f34d94..a05bb5a7e0 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,4 +1,4 @@ -import { Schema } from '@/misc/schema.js'; +import type { Schema } from '@/misc/schema.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; @@ -59,7 +59,6 @@ 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_vacuum from './endpoints/admin/vacuum.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___announcements from './endpoints/announcements.js'; @@ -253,8 +252,6 @@ import * as ep___notes_timeline from './endpoints/notes/timeline.js'; import * as ep___notes_translate from './endpoints/notes/translate.js'; import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; -import * as ep___notes_watching_create from './endpoints/notes/watching/create.js'; -import * as ep___notes_watching_delete from './endpoints/notes/watching/delete.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_read from './endpoints/notifications/read.js'; @@ -376,7 +373,6 @@ const eps = [ ['admin/unsilence-user', ep___admin_unsilenceUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], - ['admin/vacuum', ep___admin_vacuum], ['admin/delete-account', ep___admin_deleteAccount], ['admin/update-user-note', ep___admin_updateUserNote], ['announcements', ep___announcements], @@ -570,8 +566,6 @@ const eps = [ ['notes/translate', ep___notes_translate], ['notes/unrenote', ep___notes_unrenote], ['notes/user-list-timeline', ep___notes_userListTimeline], - ['notes/watching/create', ep___notes_watching_create], - ['notes/watching/delete', ep___notes_watching_delete], ['notifications/create', ep___notifications_create], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], ['notifications/read', ep___notifications_read], @@ -727,7 +721,6 @@ export interface IEndpointMeta { export interface IEndpoint { name: string; - exec: any; meta: IEndpointMeta; params: Schema; } @@ -735,7 +728,6 @@ export interface IEndpoint { const endpoints: IEndpoint[] = eps.map(([name, ep]) => { return { name: name, - exec: ep.default, meta: ep.meta || {}, params: ep.paramDef, }; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 333746f423..480ae7166e 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,6 +1,8 @@ -import define from '../../define.js'; -import { AbuseUserReports } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AbuseUserReportsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -77,33 +79,43 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, state: { type: 'string', nullable: true, default: null }, - reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "combined" }, - targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "combined" }, + reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, + targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, forwarded: { type: 'boolean', default: false }, }, required: [], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, - switch (ps.state) { - case 'resolved': query.andWhere('report.resolved = TRUE'); break; - case 'unresolved': query.andWhere('report.resolved = FALSE'); break; - } + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); - switch (ps.reporterOrigin) { - case 'local': query.andWhere('report.reporterHost IS NULL'); break; - case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break; - } + switch (ps.state) { + case 'resolved': query.andWhere('report.resolved = TRUE'); break; + case 'unresolved': query.andWhere('report.resolved = FALSE'); break; + } - switch (ps.targetUserOrigin) { - case 'local': query.andWhere('report.targetUserHost IS NULL'); break; - case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; - } + switch (ps.reporterOrigin) { + case 'local': query.andWhere('report.reporterHost IS NULL'); break; + case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break; + } - const reports = await query.take(ps.limit).getMany(); + switch (ps.targetUserOrigin) { + case 'local': query.andWhere('report.targetUserHost IS NULL'); break; + case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; + } - return await AbuseUserReports.packMany(reports); -}); + const reports = await query.take(ps.limit).getMany(); + + return await this.abuseUserReportEntityService.packMany(reports); + }); + } +} 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 5f89219991..1b173379a0 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,7 +1,11 @@ -import define from '../../../define.js'; -import { Users } from '@/models/index.js'; -import { signup } from '../../../common/signup.js'; +import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { SignupService } from '@/core/SignupService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { localUsernameSchema, passwordSchema } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -22,31 +26,42 @@ export const meta = { export const paramDef = { type: 'object', properties: { - username: Users.localUsernameSchema, - password: Users.passwordSchema, + username: localUsernameSchema, + password: passwordSchema, }, required: ['username', 'password'], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, _me) => { - const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null; - const noUsers = (await Users.countBy({ - host: IsNull(), - })) === 0; - if (!noUsers && !me?.isAdmin) throw new Error('access denied'); - - const { account, secret } = await signup({ - username: ps.username, - password: ps.password, - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true, - }); - - (res as any).token = secret; - - return res; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + private signupService: SignupService, + ) { + super(meta, paramDef, async (ps, _me) => { + const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; + const noUsers = (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0; + if (!noUsers && !me?.isAdmin) throw new Error('access denied'); + + const { account, secret } = await this.signupService.signup({ + username: ps.username, + password: ps.password, + }); + + const res = await this.userEntityService.pack(account, account, { + detail: true, + includeSecrets: true, + }); + + (res as any).token = secret; + + return res; + }); + } +} 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 629d700582..2e0222f0c6 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; -import { Users } from '@/models/index.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { QueueService } from '@/core/QueueService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -20,40 +22,52 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new Error('user not found'); - } + private queueService: QueueService, + private globalEventService: GlobalEventService, + private userSuspendService: UserSuspendService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } + if (user == null) { + throw new Error('user not found'); + } - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - } + if (user.isAdmin) { + throw new Error('cannot suspend admin'); + } - if (Users.isLocalUser(user)) { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); + if (user.isModerator) { + throw new Error('cannot suspend moderator'); + } - createDeleteAccountJob(user, { - soft: false, - }); - } else { - createDeleteAccountJob(user, { - soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する - }); - } + if (this.userEntityService.isLocalUser(user)) { + // 物理削除する前にDelete activityを送信する + await this.userSuspendService.doPostSuspend(user).catch(err => {}); - await Users.update(user.id, { - isDeleted: true, - }); + this.queueService.createDeleteAccountJob(user, { + soft: false, + }); + } else { + this.queueService.createDeleteAccountJob(user, { + soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する + }); + } - if (Users.isLocalUser(user)) { - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); + await this.usersRepository.update(user.id, { + isDeleted: true, + }); + + if (this.userEntityService.isLocalUser(user)) { + // Terminate streaming + this.globalEventService.publishUserEvent(user.id, 'terminate', {}); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index ab2c50b50f..6b32391e8d 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Ads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AdsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -24,16 +26,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - await Ads.insert({ - id: genId(), - createdAt: new Date(), - expiresAt: new Date(ps.expiresAt), - url: ps.url, - imageUrl: ps.imageUrl, - priority: ps.priority, - ratio: ps.ratio, - place: ps.place, - memo: ps.memo, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.adsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + expiresAt: new Date(ps.expiresAt), + url: ps.url, + imageUrl: ps.imageUrl, + priority: ps.priority, + ratio: ps.ratio, + place: ps.place, + memo: ps.memo, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 0ead2be005..7abefe156b 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Ads } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AdsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -26,10 +28,18 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const ad = await Ads.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const ad = await this.adsRepository.findOneBy({ id: ps.id }); - if (ad == null) throw new ApiError(meta.errors.noSuchAd); + if (ad == null) throw new ApiError(meta.errors.noSuchAd); - await Ads.delete(ad.id); -}); + await this.adsRepository.delete(ad.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 74f154f272..efece31bbf 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Ads } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AdsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -20,11 +22,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId) - .andWhere('ad.expiresAt > :now', { now: new Date() }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, - const ads = await query.take(ps.limit).getMany(); + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId) + .andWhere('ad.expiresAt > :now', { now: new Date() }); - return ads; -}); + const ads = await query.take(ps.limit).getMany(); + + return ads; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 650f8670e3..098a593379 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Ads } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AdsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -33,18 +35,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const ad = await Ads.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private adsRepository: AdsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const ad = await this.adsRepository.findOneBy({ id: ps.id }); - if (ad == null) throw new ApiError(meta.errors.noSuchAd); + if (ad == null) throw new ApiError(meta.errors.noSuchAd); - await Ads.update(ad.id, { - url: ps.url, - place: ps.place, - priority: ps.priority, - ratio: ps.ratio, - memo: ps.memo, - imageUrl: ps.imageUrl, - expiresAt: new Date(ps.expiresAt), - }); -}); + await this.adsRepository.update(ad.id, { + url: ps.url, + place: ps.place, + priority: ps.priority, + ratio: ps.ratio, + memo: ps.memo, + imageUrl: ps.imageUrl, + expiresAt: new Date(ps.expiresAt), + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 33076b6d30..ee07170d62 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Announcements } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AnnouncementsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -55,15 +57,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const announcement = await Announcements.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: null, - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }).then(x => Announcements.findOneByOrFail(x.identifiers[0])); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, - return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null }); -}); + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const announcement = await this.announcementsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: null, + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); + + return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index c17765f4fc..9a67bdb1aa 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Announcements } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AnnouncementsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -26,10 +28,18 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - await Announcements.delete(announcement.id); -}); + await this.announcementsRepository.delete(announcement.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7a5758d75b..35c14abda2 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,7 +1,9 @@ -import { Announcements, AnnouncementReads } from '@/models/index.js'; -import { Announcement } from '@/models/entities/announcement.js'; -import define from '../../../define.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; +import type { Announcement } from '@/models/entities/Announcement.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -64,26 +66,39 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, - const announcements = await query.take(ps.limit).getMany(); + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, - const reads = new Map(); + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - for (const announcement of announcements) { - reads.set(announcement, await AnnouncementReads.countBy({ - announcementId: announcement.id, - })); - } + const announcements = await query.take(ps.limit).getMany(); + + const reads = new Map(); - return announcements.map(announcement => ({ - id: announcement.id, - createdAt: announcement.createdAt.toISOString(), - updatedAt: announcement.updatedAt?.toISOString() ?? null, - title: announcement.title, - text: announcement.text, - imageUrl: announcement.imageUrl, - reads: reads.get(announcement)!, - })); -}); + for (const announcement of announcements) { + reads.set(announcement, await this.announcementReadsRepository.countBy({ + announcementId: announcement.id, + })); + } + + return announcements.map(announcement => ({ + id: announcement.id, + createdAt: announcement.createdAt.toISOString(), + updatedAt: announcement.updatedAt?.toISOString() ?? null, + title: announcement.title, + text: announcement.text, + imageUrl: announcement.imageUrl, + reads: reads.get(announcement)!, + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 61ce106d88..38358dff10 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Announcements } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AnnouncementsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -29,15 +31,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - await Announcements.update(announcement.id, { - updatedAt: new Date(), - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }); -}); + await this.announcementsRepository.update(announcement.id, { + updatedAt: new Date(), + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index 2d7ef2f236..c8b67fe1c0 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -1,6 +1,8 @@ -import { Users } from '@/models/index.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DeleteAccountService } from '@/core/DeleteAccountService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -21,11 +23,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneByOrFail({ id: ps.userId }); - if (user.isDeleted) { - return; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private deleteAccountService: DeleteAccountService, + ) { + super(meta, paramDef, async (ps) => { + const user = await this.usersRepository.findOneByOrFail({ id: ps.userId }); + if (user.isDeleted) { + return; + } - await deleteAccount(user); -}); + await this.deleteAccountService.deleteAccount(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 dc1976624d..051e4c60fb 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 @@ -1,6 +1,8 @@ -import define from '../../define.js'; -import { deleteFile } from '@/services/drive/delete-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,12 +20,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userId: ps.userId, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - for (const file of files) { - deleteFile(file); + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = await this.driveFilesRepository.findBy({ + userId: ps.userId, + }); + + for (const file of files) { + this.driveService.deleteFile(file); + } + }); } -}); +} 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 index a4b29770e1..770bade06d 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { DI } from '@/di-symbols.js'; + export const meta = { tags: ['admin'], @@ -19,29 +21,39 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (!Users.isLocalUser(user)) { - throw new Error('user is not local user'); - } - - /*if (user.isAdmin) { +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + 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 Users.update(user.id, { - driveCapacityOverrideMb: ps.overrideMb, - }); + await this.usersRepository.update(user.id, { + driveCapacityOverrideMb: ps.overrideMb, + }); - insertModerationLog(me, 'change-drive-capacity-override', { - targetId: user.id, - }); -}); + this.moderationLogService.insertModerationLog(me, 'change-drive-capacity-override', { + targetId: user.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index bab149532e..2cc4e70e55 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -1,5 +1,6 @@ -import define from '../../../define.js'; -import { createCleanRemoteFilesJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], @@ -15,6 +16,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - createCleanRemoteFilesJob(); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createCleanRemoteFilesJob(); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index 3db942e6cd..3927a89f90 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -1,7 +1,9 @@ import { IsNull } from 'typeorm'; -import define from '../../../define.js'; -import { deleteFile } from '@/services/drive/delete-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -17,12 +19,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userId: IsNull(), - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - for (const file of files) { - deleteFile(file); + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = await this.driveFilesRepository.findBy({ + userId: IsNull(), + }); + + for (const file of files) { + this.driveService.deleteFile(file); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index ba32aac431..88529ab0aa 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,6 +1,8 @@ -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -39,32 +41,42 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (ps.userId) { - query.andWhere('file.userId = :userId', { userId: ps.userId }); - } else { - if (ps.origin === 'local') { - query.andWhere('file.userHost IS NULL'); - } else if (ps.origin === 'remote') { - query.andWhere('file.userHost IS NOT NULL'); - } + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId); - if (ps.hostname) { - query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); - } - } + if (ps.userId) { + query.andWhere('file.userId = :userId', { userId: ps.userId }); + } else { + if (ps.origin === 'local') { + query.andWhere('file.userHost IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('file.userHost IS NOT NULL'); + } - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } + if (ps.hostname) { + query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); + } + } - const files = await query.take(ps.limit).getMany(); + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } - return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true }); -}); + const files = await query.take(ps.limit).getMany(); + + return await this.driveFileEntityService.packMany(files, { detail: true, withUser: true, self: true }); + }); + } +} 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 e9117a23c8..45ea9cdb50 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 @@ -1,5 +1,7 @@ -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -169,25 +171,33 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const file = ps.fileId ? await DriveFiles.findOneBy({ id: ps.fileId }) : await DriveFiles.findOne({ - where: [{ - url: ps.url, - }, { - thumbnailUrl: ps.url, - }, { - webpublicUrl: ps.url, - }], - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({ + where: [{ + url: ps.url, + }, { + thumbnailUrl: ps.url, + }, { + webpublicUrl: ps.url, + }], + }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } - if (!me.isAdmin) { - delete file.requestIp; - delete file.requestHeaders; - } + if (!me.isAdmin) { + delete file.requestIp; + delete file.requestHeaders; + } - return file; -}); + return file; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 232fbbd573..0b6e744ef8 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { In } from 'typeorm'; -import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -24,18 +24,31 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await Emojis.update(emoji.id, { - updatedAt: new Date(), - aliases: [...new Set(emoji.aliases.concat(ps.aliases))], +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const emojis = await this.emojisRepository.findBy({ + id: In(ps.ids), + }); + + for (const emoji of emojis) { + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + aliases: [...new Set(emoji.aliases.concat(ps.aliases))], + }); + } + + await this.db.queryResultCache!.remove(['meta_emojis']); }); } - - await db.queryResultCache!.remove(['meta_emojis']); -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 67349c24e0..daa57e8eb2 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; -import { Emojis, DriveFiles } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { ApiError } from '../../../error.js'; +import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; -import { publishBroadcastStream } from '@/services/stream.js'; -import { db } from '@/db/postgre.js'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository, EmojisRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], @@ -30,37 +33,58 @@ export const paramDef = { required: ['fileId'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - - const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; - - const emoji = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: name, - category: null, - host: null, - aliases: [], - originalUrl: file.url, - publicUrl: file.webpublicUrl ?? file.url, - type: file.webpublicType ?? file.type, - }).then(x => Emojis.findOneByOrFail(x.identifiers[0])); - - await db.queryResultCache!.remove(['meta_emojis']); - - publishBroadcastStream('emojiAdded', { - emoji: await Emojis.pack(emoji.id), - }); - - insertModerationLog(me, 'addEmoji', { - emojiId: emoji.id, - }); - - return { - id: emoji.id, - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private emojiEntityService: EmojiEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + + if (file == null) throw new ApiError(meta.errors.noSuchFile); + + const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; + + const emoji = await this.emojisRepository.insert({ + id: this.idService.genId(), + updatedAt: new Date(), + name: name, + category: null, + host: null, + aliases: [], + originalUrl: file.url, + publicUrl: file.webpublicUrl ?? file.url, + type: file.webpublicType ?? file.type, + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + + await this.db.queryResultCache!.remove(['meta_emojis']); + + this.globalEventService.publishBroadcastStream('emojiAdded', { + emoji: await this.emojiEntityService.pack(emoji.id), + }); + + this.moderationLogService.insertModerationLog(me, 'addEmoji', { + emojiId: emoji.id, + }); + + return { + id: emoji.id, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 7010ade0d8..08d40834c1 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { DI } from '@/di-symbols.js'; +import { DriveService } from '@/core/DriveService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { publishBroadcastStream } from '@/services/stream.js'; -import { db } from '@/db/postgre.js'; export const meta = { tags: ['admin'], @@ -42,41 +45,59 @@ export const paramDef = { required: ['emojiId'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneBy({ id: ps.emojiId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, - if (emoji == null) { - throw new ApiError(meta.errors.noSuchEmoji); - } + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, - let driveFile: DriveFile; + private emojiEntityService: EmojiEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId }); - try { - // Create file - driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); - } catch (e) { - throw new ApiError(); - } + if (emoji == null) { + throw new ApiError(meta.errors.noSuchEmoji); + } + + let driveFile: DriveFile; + + try { + // Create file + driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); + } catch (e) { + throw new ApiError(); + } - const copied = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emoji.name, - host: null, - aliases: [], - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, - }).then(x => Emojis.findOneByOrFail(x.identifiers[0])); - - await db.queryResultCache!.remove(['meta_emojis']); - - publishBroadcastStream('emojiAdded', { - emoji: await Emojis.pack(copied.id), - }); - - return { - id: copied.id, - }; -}); + const copied = await this.emojisRepository.insert({ + id: this.idService.genId(), + updatedAt: new Date(), + name: emoji.name, + host: null, + aliases: [], + originalUrl: driveFile.url, + publicUrl: driveFile.webpublicUrl ?? driveFile.url, + type: driveFile.webpublicType ?? driveFile.type, + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + + await this.db.queryResultCache!.remove(['meta_emojis']); + + this.globalEventService.publishBroadcastStream('emojiAdded', { + emoji: await this.emojiEntityService.pack(copied.id), + }); + + return { + id: copied.id, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 93a6c4e4e2..81b095cb57 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,9 +1,9 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { In } from 'typeorm'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -22,19 +22,34 @@ export const paramDef = { required: ['ids'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const emojis = await this.emojisRepository.findBy({ + id: In(ps.ids), + }); - for (const emoji of emojis) { - await Emojis.delete(emoji.id); + for (const emoji of emojis) { + await this.emojisRepository.delete(emoji.id); - await db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache!.remove(['meta_emojis']); - insertModerationLog(me, 'deleteEmoji', { - emoji: emoji, + this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { + emoji: emoji, + }); + } }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 67dbf28d85..e4278dc33a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; export const meta = { tags: ['admin'], @@ -27,17 +29,32 @@ export const paramDef = { required: ['id'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); + if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - await Emojis.delete(emoji.id); + await this.emojisRepository.delete(emoji.id); - await db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache!.remove(['meta_emojis']); - insertModerationLog(me, 'deleteEmoji', { - emoji: emoji, - }); -}); + this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { + emoji: emoji, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 3f03dc2da4..6fe492cb75 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; -import { createImportCustomEmojisJob } from '@/queue/index.js'; -import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -17,6 +17,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createImportCustomEmojisJob(user, ps.fileId); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createImportCustomEmojisJob(me, ps.fileId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index d16689a280..9d6fa53417 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,7 +1,10 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -69,23 +72,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, - if (ps.host == null) { - q.andWhere(`emoji.host IS NOT NULL`); - } else { - q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) }); - } + private utilityService: UtilityService, + private queryService: QueryService, + private emojiEntityService: EmojiEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); - if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); - } + if (ps.host == null) { + q.andWhere('emoji.host IS NOT NULL'); + } else { + q.andWhere('emoji.host = :host', { host: this.utilityService.toPuny(ps.host) }); + } - const emojis = await q - .orderBy('emoji.id', 'DESC') - .take(ps.limit) - .getMany(); + if (ps.query) { + q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); + } - return Emojis.packMany(emojis); -}); + const emojis = await q + .orderBy('emoji.id', 'DESC') + .take(ps.limit) + .getMany(); + + return this.emojiEntityService.packMany(emojis); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 6192978fad..736d664cc3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; -import { Emoji } from '@/models/entities/emoji.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -63,27 +65,37 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) - .andWhere(`emoji.host IS NULL`); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, - let emojis: Emoji[]; + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) + .andWhere('emoji.host IS NULL'); - if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); - //const emojis = await q.take(ps.limit).getMany(); + let emojis: Emoji[]; - emojis = await q.getMany(); + if (ps.query) { + //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); + //const emojis = await q.take(ps.limit).getMany(); - emojis = emojis.filter(emoji => - emoji.name.includes(ps.query!) || - emoji.aliases.some(a => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!)); + emojis = await q.getMany(); - emojis.splice(ps.limit + 1); - } else { - emojis = await q.take(ps.limit).getMany(); - } + emojis = emojis.filter(emoji => + emoji.name.includes(ps.query!) || + emoji.aliases.some(a => a.includes(ps.query!)) || + emoji.category?.includes(ps.query!)); + + emojis.splice(ps.limit + 1); + } else { + emojis = await q.take(ps.limit).getMany(); + } - return Emojis.packMany(emojis); -}); + return this.emojiEntityService.packMany(emojis); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index a4da40fffd..d6c70eaae7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { In } from 'typeorm'; -import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -24,18 +24,31 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await Emojis.update(emoji.id, { - updatedAt: new Date(), - aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)), +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const emojis = await this.emojisRepository.findBy({ + id: In(ps.ids), + }); + + for (const emoji of emojis) { + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)), + }); + } + + await this.db.queryResultCache!.remove(['meta_emojis']); }); } - - await db.queryResultCache!.remove(['meta_emojis']); -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index ae3b190f40..c438b7f9b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { In } from 'typeorm'; -import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -24,14 +24,27 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - await Emojis.update({ - id: In(ps.ids), - }, { - updatedAt: new Date(), - aliases: ps.aliases, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.emojisRepository.update({ + id: In(ps.ids), + }, { + updatedAt: new Date(), + aliases: ps.aliases, + }); - await db.queryResultCache!.remove(['meta_emojis']); -}); + await this.db.queryResultCache!.remove(['meta_emojis']); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index cff58d6170..4a9b31fd28 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; -import { In } from 'typeorm'; -import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -26,14 +26,27 @@ export const paramDef = { required: ['ids'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - await Emojis.update({ - id: In(ps.ids), - }, { - updatedAt: new Date(), - category: ps.category, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.emojisRepository.update({ + id: In(ps.ids), + }, { + updatedAt: new Date(), + category: ps.category, + }); - await db.queryResultCache!.remove(['meta_emojis']); -}); + await this.db.queryResultCache!.remove(['meta_emojis']); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 5b547b3b79..e6eb9eb9a6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; -import { Emojis } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojisRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { db } from '@/db/postgre.js'; export const meta = { tags: ['admin'], @@ -35,18 +37,31 @@ export const paramDef = { required: ['id', 'name', 'aliases'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const emoji = await Emojis.findOneBy({ id: ps.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); + if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - await Emojis.update(emoji.id, { - updatedAt: new Date(), - name: ps.name, - category: ps.category, - aliases: ps.aliases, - }); + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + name: ps.name, + category: ps.category, + aliases: ps.aliases, + }); - await db.queryResultCache!.remove(['meta_emojis']); -}); + await this.db.queryResultCache!.remove(['meta_emojis']); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index da54201473..789838661c 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { deleteFile } from '@/services/drive/delete-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,12 +20,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userHost: ps.host, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - for (const file of files) { - deleteFile(file); + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = await this.driveFilesRepository.findBy({ + userHost: ps.host, + }); + + for (const file of files) { + this.driveService.deleteFile(file); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index cb2be5ab37..476b821523 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InstancesRepository } from '@/models/index.js'; +import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -19,12 +21,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, - if (instance == null) { - throw new Error('instance not found'); - } + private utilityService: UtilityService, + private fetchInstanceMetadataService: FetchInstanceMetadataService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); + + if (instance == null) { + throw new Error('instance not found'); + } - fetchInstanceMetadata(instance, true); -}); + this.fetchInstanceMetadataService.fetchInstanceMetadata(instance, true); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index b7ee27db64..67165dc47e 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import deleteFollowing from '@/services/following/delete.js'; -import { Followings, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,17 +20,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const followings = await Followings.findBy({ - followerHost: ps.host, - }); - - const pairs = await Promise.all(followings.map(f => Promise.all([ - Users.findOneByOrFail({ id: f.followerId }), - Users.findOneByOrFail({ id: f.followeeId }), - ]))); - - for (const pair of pairs) { - deleteFollowing(pair[0], pair[1]); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private followingsRepository: FollowingsRepository, + + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const followings = await this.followingsRepository.findBy({ + followerHost: ps.host, + }); + + const pairs = await Promise.all(followings.map(f => Promise.all([ + this.usersRepository.findOneByOrFail({ id: f.followerId }), + this.usersRepository.findOneByOrFail({ id: f.followeeId }), + ]))); + + for (const pair of pairs) { + this.userFollowingService.unfollow(pair[0], pair[1]); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 278131fb37..b9eade5b40 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InstancesRepository } from '@/models/index.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -19,14 +21,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, - if (instance == null) { - throw new Error('instance not found'); - } + private utilityService: UtilityService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); + + if (instance == null) { + throw new Error('instance not found'); + } - Instances.update({ host: toPuny(ps.host) }, { - isSuspended: ps.isSuspended, - }); -}); + this.instancesRepository.update({ host: this.utilityService.toPuny(ps.host) }, { + isSuspended: ps.isSuspended, + }); + }); + } +} 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 dd16473f30..e53d0bfcea 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 @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -15,14 +17,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const stats = await db.query(`SELECT * FROM pg_indexes;`).then(recs => { - const res = [] as { tablename: string; indexname: string; }[]; - for (const rec of recs) { - res.push(rec); - } - return res; - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + ) { + super(meta, paramDef, async () => { + const stats = await this.db.query('SELECT * FROM pg_indexes;').then(recs => { + const res = [] as { tablename: string; indexname: string; }[]; + for (const rec of recs) { + res.push(rec); + } + return res; + }); - return stats; -}); + return stats; + }); + } +} 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 aca2540fd5..41014cb167 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 @@ -1,5 +1,7 @@ -import { db } from '@/db/postgre.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -26,24 +28,31 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const sizes = await - db.query(` +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + ) { + super(meta, paramDef, async () => { + const sizes = await this.db.query(` SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size" FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') AND C.relkind <> 'i' AND nspname !~ '^pg_toast';`) - .then(recs => { - const res = {} as Record; - for (const rec of recs) { - res[rec.table] = { - count: parseInt(rec.count, 10), - size: parseInt(rec.size, 10), - }; - } - return res; - }); + .then(recs => { + const res = {} as Record; + for (const rec of recs) { + res[rec.table] = { + count: parseInt(rec.count, 10), + size: parseInt(rec.size, 10), + }; + } + return res; + }); - return sizes; -}); + return sizes; + }); + } +} 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 e8b9cb3b09..eddaade919 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 @@ -1,5 +1,7 @@ -import { UserIps } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserIpsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -17,15 +19,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const ips = await UserIps.find({ - where: { userId: ps.userId }, - order: { createdAt: 'DESC' }, - take: 30, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userIpsRepository) + private userIpsRepository: UserIpsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const ips = await this.userIpsRepository.find({ + where: { userId: ps.userId }, + order: { createdAt: 'DESC' }, + take: 30, + }); - return ips.map(x => ({ - ip: x.ip, - createdAt: x.createdAt.toISOString(), - })); -}); + return ips.map(x => ({ + ip: x.ip, + createdAt: x.createdAt.toISOString(), + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts index 7e950cf87b..5fe341e5ca 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite.ts @@ -1,7 +1,9 @@ import rndstr from 'rndstr'; -import define from '../../define.js'; -import { RegistrationTickets } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistrationTicketsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -31,19 +33,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const code = rndstr({ - length: 8, - chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) - }); - - await RegistrationTickets.insert({ - id: genId(), - createdAt: new Date(), - code, - }); - - return { - code, - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async () => { + const code = rndstr({ + length: 8, + chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) + }); + + await this.registrationTicketsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + code, + }); + + return { + code, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 8746119687..615c0a0e70 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,7 +1,9 @@ -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Inject, Injectable } from '@nestjs/common'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import define from '../../define.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['meta'], @@ -340,91 +342,101 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, - return { - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - version: config.version, - name: instance.name, - uri: config.url, - description: instance.description, - langs: instance.langs, - tosUrl: instance.ToSUrl, - 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, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl, - bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - enableEmail: instance.enableEmail, - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, - enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - useStarForReactionFallback: instance.useStarForReactionFallback, - pinnedUsers: instance.pinnedUsers, - hiddenTags: instance.hiddenTags, - blockedHosts: instance.blockedHosts, - hcaptchaSecretKey: instance.hcaptchaSecretKey, - recaptchaSecretKey: instance.recaptchaSecretKey, - sensitiveMediaDetection: instance.sensitiveMediaDetection, - sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, - setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, - enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, - proxyAccountId: instance.proxyAccountId, - twitterConsumerKey: instance.twitterConsumerKey, - twitterConsumerSecret: instance.twitterConsumerSecret, - githubClientId: instance.githubClientId, - githubClientSecret: instance.githubClientSecret, - discordClientId: instance.discordClientId, - discordClientSecret: instance.discordClientSecret, - summalyProxy: instance.summalyProxy, - email: instance.email, - smtpSecure: instance.smtpSecure, - smtpHost: instance.smtpHost, - smtpPort: instance.smtpPort, - smtpUser: instance.smtpUser, - smtpPass: instance.smtpPass, - swPrivateKey: instance.swPrivateKey, - useObjectStorage: instance.useObjectStorage, - objectStorageBaseUrl: instance.objectStorageBaseUrl, - objectStorageBucket: instance.objectStorageBucket, - objectStoragePrefix: instance.objectStoragePrefix, - objectStorageEndpoint: instance.objectStorageEndpoint, - objectStorageRegion: instance.objectStorageRegion, - objectStoragePort: instance.objectStoragePort, - objectStorageAccessKey: instance.objectStorageAccessKey, - objectStorageSecretKey: instance.objectStorageSecretKey, - objectStorageUseSSL: instance.objectStorageUseSSL, - objectStorageUseProxy: instance.objectStorageUseProxy, - objectStorageSetPublicRead: instance.objectStorageSetPublicRead, - objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, - deeplAuthKey: instance.deeplAuthKey, - deeplIsPro: instance.deeplIsPro, - enableIpLogging: instance.enableIpLogging, - enableActiveEmailValidation: instance.enableActiveEmailValidation, - }; -}); + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.metaService.fetch(true); + + return { + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, + version: this.config.version, + name: instance.name, + uri: this.config.url, + description: instance.description, + langs: instance.langs, + tosUrl: instance.ToSUrl, + 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, + hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableRecaptcha: instance.enableRecaptcha, + recaptchaSiteKey: instance.recaptchaSiteKey, + swPublickey: instance.swPublicKey, + themeColor: instance.themeColor, + mascotImageUrl: instance.mascotImageUrl, + bannerUrl: instance.bannerUrl, + errorImageUrl: instance.errorImageUrl, + iconUrl: instance.iconUrl, + backgroundImageUrl: instance.backgroundImageUrl, + logoImageUrl: instance.logoImageUrl, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため + defaultLightTheme: instance.defaultLightTheme, + defaultDarkTheme: instance.defaultDarkTheme, + enableEmail: instance.enableEmail, + enableTwitterIntegration: instance.enableTwitterIntegration, + enableGithubIntegration: instance.enableGithubIntegration, + enableDiscordIntegration: instance.enableDiscordIntegration, + enableServiceWorker: instance.enableServiceWorker, + translatorAvailable: instance.deeplAuthKey != null, + pinnedPages: instance.pinnedPages, + pinnedClipId: instance.pinnedClipId, + cacheRemoteFiles: instance.cacheRemoteFiles, + useStarForReactionFallback: instance.useStarForReactionFallback, + pinnedUsers: instance.pinnedUsers, + hiddenTags: instance.hiddenTags, + blockedHosts: instance.blockedHosts, + hcaptchaSecretKey: instance.hcaptchaSecretKey, + recaptchaSecretKey: instance.recaptchaSecretKey, + sensitiveMediaDetection: instance.sensitiveMediaDetection, + sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, + setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + proxyAccountId: instance.proxyAccountId, + twitterConsumerKey: instance.twitterConsumerKey, + twitterConsumerSecret: instance.twitterConsumerSecret, + githubClientId: instance.githubClientId, + githubClientSecret: instance.githubClientSecret, + discordClientId: instance.discordClientId, + discordClientSecret: instance.discordClientSecret, + summalyProxy: instance.summalyProxy, + email: instance.email, + smtpSecure: instance.smtpSecure, + smtpHost: instance.smtpHost, + smtpPort: instance.smtpPort, + smtpUser: instance.smtpUser, + smtpPass: instance.smtpPass, + swPrivateKey: instance.swPrivateKey, + useObjectStorage: instance.useObjectStorage, + objectStorageBaseUrl: instance.objectStorageBaseUrl, + objectStorageBucket: instance.objectStorageBucket, + objectStoragePrefix: instance.objectStoragePrefix, + objectStorageEndpoint: instance.objectStorageEndpoint, + objectStorageRegion: instance.objectStorageRegion, + objectStoragePort: instance.objectStoragePort, + objectStorageAccessKey: instance.objectStorageAccessKey, + objectStorageSecretKey: instance.objectStorageSecretKey, + objectStorageUseSSL: instance.objectStorageUseSSL, + objectStorageUseProxy: instance.objectStorageUseProxy, + objectStorageSetPublicRead: instance.objectStorageSetPublicRead, + objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, + deeplAuthKey: instance.deeplAuthKey, + deeplIsPro: instance.deeplIsPro, + enableIpLogging: instance.enableIpLogging, + enableActiveEmailValidation: instance.enableActiveEmailValidation, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts index 7b209c2d99..fe200da6ad 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Users } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,20 +20,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.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'); +@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 }); + }); } - - await Users.update(user.id, { - isModerator: true, - }); - - 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 index a01e9f3c69..3dc7158ba9 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; -import { Users } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,16 +20,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new Error('user not found'); - } + 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 Users.update(user.id, { - isModerator: false, - }); + await this.usersRepository.update(user.id, { + isModerator: false, + }); - publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); -}); + this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 68a17867b2..a179f163df 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PromoNotesRepository } from '@/models/index.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getNote } from '../../../common/getters.js'; -import { PromoNotes } from '@/models/index.js'; export const meta = { tags: ['admin'], @@ -34,21 +36,31 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.promoNotesRepository) + private promoNotesRepository: PromoNotesRepository, - const exist = await PromoNotes.findOneBy({ noteId: note.id }); + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyPromoted); - } + const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyPromoted); + } - await PromoNotes.insert({ - noteId: note.id, - expiresAt: new Date(ps.expiresAt), - userId: note.userId, - }); -}); + await this.promoNotesRepository.insert({ + noteId: note.id, + expiresAt: new Date(ps.expiresAt), + userId: note.userId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index 8f015c280a..9129f53f06 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -1,6 +1,7 @@ -import define from '../../../define.js'; -import { destroy } from '@/queue/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], @@ -16,8 +17,16 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - destroy(); +@Injectable() +export default class extends Endpoint { + constructor( + private moderationLogService: ModerationLogService, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.destroy(); - insertModerationLog(me, 'clearQueue'); -}); + this.moderationLogService.insertModerationLog(me, 'clearQueue'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 70f7d77de4..4b5be70d56 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -1,6 +1,7 @@ -import { deliverQueue } from '@/queue/queues.js'; import { URL } from 'node:url'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DeliverQueue } from '@/core/queue/QueueModule.js'; export const meta = { tags: ['admin'], @@ -39,21 +40,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const jobs = await deliverQueue.getJobs(['delayed']); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.to).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject('queue:deliver') public deliverQueue: DeliverQueue, + ) { + super(meta, paramDef, async (ps, me) => { + const jobs = await this.deliverQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; - res.sort((a, b) => b[1] - a[1]); + for (const job of jobs) { + const host = new URL(job.data.to).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } - return res; -}); + res.sort((a, b) => b[1] - a[1]); + + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index 2235ce8f97..715974e917 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -1,6 +1,7 @@ import { URL } from 'node:url'; -import define from '../../../define.js'; -import { inboxQueue } from '@/queue/queues.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InboxQueue } from '@/core/queue/QueueModule.js'; export const meta = { tags: ['admin'], @@ -39,21 +40,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const jobs = await inboxQueue.getJobs(['delayed']); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.signature.keyId).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject('queue:inbox') public inboxQueue: InboxQueue, + ) { + super(meta, paramDef, async (ps, me) => { + const jobs = await this.inboxQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; - res.sort((a, b) => b[1] - a[1]); + for (const job of jobs) { + const host = new URL(job.data.signature.keyId).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } - return res; -}); + res.sort((a, b) => b[1] - a[1]); + + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 988b5a5e35..f2ca81a8da 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -1,5 +1,6 @@ -import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/queue/QueueModule.js'; export const meta = { tags: ['admin'], @@ -38,16 +39,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - const dbJobCounts = await dbQueue.getJobCounts(); - const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject('queue:system') public systemQueue: SystemQueue, + @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:deliver') public deliverQueue: DeliverQueue, + @Inject('queue:inbox') public inboxQueue: InboxQueue, + @Inject('queue:db') public dbQueue: DbQueue, + @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + ) { + super(meta, paramDef, async (ps, me) => { + const deliverJobCounts = await this.deliverQueue.getJobCounts(); + const inboxJobCounts = await this.inboxQueue.getJobCounts(); + const dbJobCounts = await this.dbQueue.getJobCounts(); + const objectStorageJobCounts = await this.objectStorageQueue.getJobCounts(); - return { - deliver: deliverJobCounts, - inbox: inboxJobCounts, - db: dbJobCounts, - objectStorage: objectStorageJobCounts, - }; -}); + return { + deliver: deliverJobCounts, + inbox: inboxJobCounts, + db: dbJobCounts, + objectStorage: objectStorageJobCounts, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 348e9baca1..32ad79918f 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -1,6 +1,7 @@ import { URL } from 'node:url'; -import define from '../../../define.js'; -import { addRelay } from '@/services/relay.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RelayService } from '@/core/RelayService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -54,12 +55,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - try { - if (new URL(ps.inbox).protocol !== 'https:') throw 'https only'; - } catch { - throw new ApiError(meta.errors.invalidUrl); - } +@Injectable() +export default class extends Endpoint { + constructor( + private relayService: RelayService, + ) { + super(meta, paramDef, async (ps, me) => { + try { + if (new URL(ps.inbox).protocol !== 'https:') throw 'https only'; + } catch { + throw new ApiError(meta.errors.invalidUrl); + } - return await addRelay(ps.inbox); -}); + return await this.relayService.addRelay(ps.inbox); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index 89ec651e61..079b351add 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -1,5 +1,6 @@ -import define from '../../../define.js'; -import { listRelay } from '@/services/relay.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RelayService } from '@/core/RelayService.js'; export const meta = { tags: ['admin'], @@ -46,6 +47,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - return await listRelay(); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private relayService: RelayService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.relayService.listRelay(); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index b59cf72c58..9dc4105d14 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -1,5 +1,6 @@ -import define from '../../../define.js'; -import { removeRelay } from '@/services/relay.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RelayService } from '@/core/RelayService.js'; export const meta = { tags: ['admin'], @@ -17,6 +18,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - return await removeRelay(ps.inbox); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private relayService: RelayService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.relayService.removeRelay(ps.inbox); + }); + } +} 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 be4c2dceed..7446746b45 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import rndstr from 'rndstr'; -import { Users, UserProfiles } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -32,29 +34,40 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new Error('user not found'); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); - if (user.isAdmin) { - throw new Error('cannot reset password of admin'); - } + if (user == null) { + throw new Error('user not found'); + } - const passwd = rndstr('a-zA-Z0-9', 8); + if (user.isAdmin) { + throw new Error('cannot reset password of admin'); + } - // Generate hash of password - const hash = bcrypt.hashSync(passwd); + const passwd = rndstr('a-zA-Z0-9', 8); - await UserProfiles.update({ - userId: user.id, - }, { - password: hash, - }); + // Generate hash of password + const hash = bcrypt.hashSync(passwd); - return { - password: passwd, - }; -}); + await this.userProfilesRepository.update({ + userId: user.id, + }, { + password: hash, + }); + + return { + password: passwd, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 3edae4a85f..b5828ae9be 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,9 +1,10 @@ -import define from '../../define.js'; -import { AbuseUserReports, Users } from '@/models/index.js'; -import { getInstanceActor } from '@/services/instance-actor.js'; -import { deliver } from '@/queue/index.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { renderFlag } from '@/remote/activitypub/renderer/flag.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -21,24 +22,41 @@ export const paramDef = { required: ['reportId'], } as const; -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const report = await AbuseUserReports.findOneByOrFail({ id: ps.reportId }); - - if (report == null) { - throw new Error('report not found'); - } +// TODO: ロジックをサービスに切り出す - if (ps.forward && report.targetUserHost != null) { - const actor = await getInstanceActor(); - const targetUser = await Users.findOneByOrFail({ id: report.targetUserId }); - - deliver(actor, renderActivity(renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox); +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + private queueService: QueueService, + private instanceActorService: InstanceActorService, + private apRendererService: ApRendererService, + ) { + super(meta, paramDef, async (ps, me) => { + const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); + + if (report == null) { + throw new Error('report not found'); + } + + if (ps.forward && report.targetUserHost != null) { + const actor = await this.instanceActorService.getInstanceActor(); + const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); + + this.queueService.deliver(actor, this.apRendererService.renderActivity(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox); + } + + await this.abuseUserReportsRepository.update(report.id, { + resolved: true, + assigneeId: me.id, + forwarded: ps.forward && report.targetUserHost != null, + }); + }); } - - await AbuseUserReports.update(report.id, { - resolved: true, - assigneeId: me.id, - forwarded: ps.forward && report.targetUserHost != null, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts index bbdd66e4c9..7434bf4c91 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -1,5 +1,6 @@ -import define from '../../define.js'; -import { sendEmail } from '@/services/send-email.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmailService } from '@/core/EmailService.js'; export const meta = { tags: ['admin'], @@ -19,6 +20,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - await sendEmail(ps.to, ps.subject, ps.text, ps.text); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private emailService: EmailService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.emailService.sendEmail(ps.to, ps.subject, ps.text, ps.text); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 85c6fb82e7..9c576dffe9 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -1,8 +1,10 @@ import * as os from 'node:os'; import si from 'systeminformation'; -import define from '../../define.js'; -import { redisClient } from '../../../../db/redis.js'; -import { db } from '@/db/postgre.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import Redis from 'ioredis'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -94,34 +96,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); - const netInterface = await si.networkInterfaceDefault(); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, - const redisServerInfo = await redisClient.info('Server'); - const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm')); - const redis_version = m?.[1]; + @Inject(DI.redis) + private redisClient: Redis.Redis, - return { - machine: os.hostname(), - os: os.platform(), - node: process.version, - psql: await db.query('SHOW server_version').then(x => x[0].server_version), - redis: redis_version, - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length, - }, - mem: { - total: memStats.total, - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - net: { - interface: netInterface, - }, - }; -}); + ) { + super(meta, paramDef, async () => { + const memStats = await si.mem(); + const fsStats = await si.fsSize(); + const netInterface = await si.networkInterfaceDefault(); + + const redisServerInfo = await this.redisClient.info('Server'); + const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm')); + const redis_version = m?.[1]; + + return { + machine: os.hostname(), + os: os.platform(), + node: process.version, + psql: await this.db.query('SHOW server_version').then(x => x[0].server_version), + redis: redis_version, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length, + }, + mem: { + total: memStats.total, + }, + fs: { + total: fsStats[0].size, + used: fsStats[0].used, + }, + net: { + interface: netInterface, + }, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 3545536aa2..2424cac425 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -1,6 +1,8 @@ -import define from '../../define.js'; -import { ModerationLogs } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -59,10 +61,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.moderationLogsRepository) + private moderationLogsRepository: ModerationLogsRepository, - const reports = await query.take(ps.limit).getMany(); + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); - return await ModerationLogs.packMany(reports); -}); + const reports = await query.take(ps.limit).getMany(); + + return await this.moderationLogEntityService.packMany(reports); + }); + } +} 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 0d866b3113..b50564210b 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,7 @@ -import { Signins, UserProfiles, Users } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -22,55 +24,69 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const [user, profile] = await Promise.all([ - Users.findOneBy({ id: ps.userId }), - UserProfiles.findOneBy({ userId: ps.userId }), - ]); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null || profile == null) { - throw new Error('user not found'); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - const _me = await Users.findOneByOrFail({ id: me.id }); - if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { - throw new Error('cannot show info of admin'); - } + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const [user, profile] = await Promise.all([ + this.usersRepository.findOneBy({ id: ps.userId }), + this.userProfilesRepository.findOneBy({ userId: ps.userId }), + ]); - if (!_me.isAdmin) { - return { - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - }; - } + if (user == null || profile == null) { + throw new Error('user not found'); + } - const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; - Object.keys(profile.integrations).forEach(integration => { - maskedKeys.forEach(key => profile.integrations[integration][key] = ''); - }); + const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); + if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { + throw new Error('cannot show info of admin'); + } - const signins = await Signins.findBy({ userId: user.id }); + if (!_me.isAdmin) { + return { + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + }; + } - return { - email: profile.email, - emailVerified: profile.emailVerified, - autoAcceptFollowed: profile.autoAcceptFollowed, - noCrawle: profile.noCrawle, - alwaysMarkNsfw: profile.alwaysMarkNsfw, - autoSensitive: profile.autoSensitive, - carefulBot: profile.carefulBot, - injectFeaturedNote: profile.injectFeaturedNote, - receiveAnnouncementEmail: profile.receiveAnnouncementEmail, - integrations: profile.integrations, - mutedWords: profile.mutedWords, - mutedInstances: profile.mutedInstances, - mutingNotificationTypes: profile.mutingNotificationTypes, - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - lastActiveDate: user.lastActiveDate, - moderationNote: profile.moderationNote, - signins, - }; -}); + const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; + Object.keys(profile.integrations).forEach(integration => { + maskedKeys.forEach(key => profile.integrations[integration][key] = ''); + }); + + const signins = await this.signinsRepository.findBy({ userId: user.id }); + + return { + email: profile.email, + emailVerified: profile.emailVerified, + autoAcceptFollowed: profile.autoAcceptFollowed, + noCrawle: profile.noCrawle, + alwaysMarkNsfw: profile.alwaysMarkNsfw, + autoSensitive: profile.autoSensitive, + carefulBot: profile.carefulBot, + injectFeaturedNote: profile.injectFeaturedNote, + receiveAnnouncementEmail: profile.receiveAnnouncementEmail, + integrations: profile.integrations, + mutedWords: profile.mutedWords, + mutedInstances: profile.mutedInstances, + mutingNotificationTypes: profile.mutingNotificationTypes, + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + lastActiveDate: user.lastActiveDate, + moderationNote: profile.moderationNote, + signins, + }; + }); + } +} 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 8e09e72d5b..8d11e3ea7a 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,7 @@ -import { Users } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -38,46 +40,54 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + 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; - } + 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; + } - switch (ps.origin) { - case 'local': query.andWhere('user.host IS NULL'); break; - case 'remote': query.andWhere('user.host IS NOT NULL'); break; - } + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } - if (ps.username) { - query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); - } + if (ps.username) { + query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); + } - if (ps.hostname) { - query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); - } + if (ps.hostname) { + query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); + } - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break; - case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break; - default: query.orderBy('user.id', 'ASC'); break; - } + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break; + default: query.orderBy('user.id', 'ASC'); break; + } - query.take(ps.limit); - query.skip(ps.offset); + query.take(ps.limit); + query.skip(ps.offset); - const users = await query.getMany(); + const users = await query.getMany(); - return await Users.packMany(users, me, { detail: true }); -}); + return await this.userEntityService.packMany(users, me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index 17b9f3b5a0..bec8f7719e 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { publishInternalEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UsersRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -19,24 +21,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot silence admin'); +@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, + }); + }); } - - await Users.update(user.id, { - isSilenced: true, - }); - - publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true }); - - 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 ed513eda08..fa057dadb6 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,10 +1,12 @@ -import define from '../../define.js'; -import deleteFollowing from '@/services/following/delete.js'; -import { Users, Followings, Notifications } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -22,64 +24,83 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - } - - await Users.update(user.id, { - isSuspended: true, - }); - - insertModerationLog(me, 'suspend', { - targetId: user.id, - }); - - // Terminate streaming - if (Users.isLocalUser(user)) { - publishUserEvent(user.id, 'terminate', {}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + private userFollowingService: UserFollowingService, + private userSuspendService: UserSuspendService, + 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 suspend admin'); + } + + if (user.isModerator) { + throw new Error('cannot suspend moderator'); + } + + await this.usersRepository.update(user.id, { + isSuspended: true, + }); + + this.moderationLogService.insertModerationLog(me, 'suspend', { + targetId: user.id, + }); + + // Terminate streaming + if (this.userEntityService.isLocalUser(user)) { + this.globalEventService.publishUserEvent(user.id, 'terminate', {}); + } + + (async () => { + await this.userSuspendService.doPostSuspend(user).catch(e => {}); + await this.#unFollowAll(user).catch(e => {}); + await this.#readAllNotify(user).catch(e => {}); + })(); + }); } - (async () => { - await doPostSuspend(user).catch(e => {}); - await unFollowAll(user).catch(e => {}); - await readAllNotify(user).catch(e => {}); - })(); -}); - -async function unFollowAll(follower: User) { - const followings = await Followings.findBy({ - followerId: follower.id, - }); - - for (const following of followings) { - const followee = await Users.findOneBy({ - id: following.followeeId, + async #unFollowAll(follower: User) { + const followings = await this.followingsRepository.findBy({ + followerId: follower.id, }); - - if (followee == null) { - throw `Cant find followee ${following.followeeId}`; + + for (const following of followings) { + const followee = await this.usersRepository.findOneBy({ + id: following.followeeId, + }); + + if (followee == null) { + throw `Cant find followee ${following.followeeId}`; + } + + await this.userFollowingService.unfollow(follower, followee, true); } - - await deleteFollowing(follower, followee, true); } -} - -async function readAllNotify(notifier: User) { - await Notifications.update({ - notifierId: notifier.id, - isRead: false, - }, { - isRead: true, - }); + + async #readAllNotify(notifier: User) { + await this.notificationsRepository.update({ + notifierId: notifier.id, + isRead: false, + }, { + isRead: true, + }); + } } diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index a4b373f5c7..b4671a2f41 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { publishInternalEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { 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'], @@ -19,20 +21,31 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); +@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, + }); + }); } - - await Users.update(user.id, { - isSilenced: false, - }); - - publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false }); - - insertModerationLog(me, 'unsilence', { - targetId: user.id, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 5cf26251be..96283d251f 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { doPostUnsuspend } from '@/services/unsuspend-user.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -19,20 +21,31 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userSuspendService: UserSuspendService, + 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'); + } + + await this.usersRepository.update(user.id, { + isSuspended: false, + }); + + this.moderationLogService.insertModerationLog(me, 'unsuspend', { + targetId: user.id, + }); + + this.userSuspendService.doPostUnsuspend(user); + }); } - - await Users.update(user.id, { - isSuspended: false, - }); - - insertModerationLog(me, 'unsuspend', { - targetId: user.id, - }); - - doPostUnsuspend(user); -}); +} 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 f14aa41050..968ed4d26d 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,8 +1,10 @@ -import { Meta } from '@/models/entities/meta.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Meta } from '@/models/entities/Meta.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; -import { db } from '@/db/postgre.js'; -import define from '../../define.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -107,340 +109,350 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const set = {} as Partial; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const set = {} as Partial; - if (typeof ps.disableRegistration === 'boolean') { - 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; - } + if (typeof ps.disableRegistration === 'boolean') { + set.disableRegistration = ps.disableRegistration; + } - if (Array.isArray(ps.pinnedUsers)) { - set.pinnedUsers = ps.pinnedUsers.filter(Boolean); - } - - if (Array.isArray(ps.hiddenTags)) { - set.hiddenTags = ps.hiddenTags.filter(Boolean); - } + if (typeof ps.disableLocalTimeline === 'boolean') { + set.disableLocalTimeline = ps.disableLocalTimeline; + } - if (Array.isArray(ps.blockedHosts)) { - set.blockedHosts = ps.blockedHosts.filter(Boolean); - } + if (typeof ps.disableGlobalTimeline === 'boolean') { + set.disableGlobalTimeline = ps.disableGlobalTimeline; + } - if (ps.themeColor !== undefined) { - set.themeColor = ps.themeColor; - } - - if (ps.mascotImageUrl !== undefined) { - set.mascotImageUrl = ps.mascotImageUrl; - } + if (typeof ps.useStarForReactionFallback === 'boolean') { + set.useStarForReactionFallback = ps.useStarForReactionFallback; + } - if (ps.bannerUrl !== undefined) { - set.bannerUrl = ps.bannerUrl; - } - - if (ps.iconUrl !== undefined) { - set.iconUrl = ps.iconUrl; - } - - if (ps.backgroundImageUrl !== undefined) { - set.backgroundImageUrl = ps.backgroundImageUrl; - } - - if (ps.logoImageUrl !== undefined) { - set.logoImageUrl = ps.logoImageUrl; - } - - if (ps.name !== undefined) { - set.name = ps.name; - } - - if (ps.description !== undefined) { - set.description = ps.description; - } - - if (ps.defaultLightTheme !== undefined) { - set.defaultLightTheme = ps.defaultLightTheme; - } - - if (ps.defaultDarkTheme !== undefined) { - set.defaultDarkTheme = ps.defaultDarkTheme; - } - - if (ps.localDriveCapacityMb !== undefined) { - set.localDriveCapacityMb = ps.localDriveCapacityMb; - } - - if (ps.remoteDriveCapacityMb !== undefined) { - set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; - } - - if (ps.cacheRemoteFiles !== undefined) { - set.cacheRemoteFiles = ps.cacheRemoteFiles; - } - - if (ps.emailRequiredForSignup !== undefined) { - set.emailRequiredForSignup = ps.emailRequiredForSignup; - } - - if (ps.enableHcaptcha !== undefined) { - set.enableHcaptcha = ps.enableHcaptcha; - } - - if (ps.hcaptchaSiteKey !== undefined) { - set.hcaptchaSiteKey = ps.hcaptchaSiteKey; - } - - if (ps.hcaptchaSecretKey !== undefined) { - set.hcaptchaSecretKey = ps.hcaptchaSecretKey; - } - - if (ps.enableRecaptcha !== undefined) { - set.enableRecaptcha = ps.enableRecaptcha; - } - - if (ps.recaptchaSiteKey !== undefined) { - set.recaptchaSiteKey = ps.recaptchaSiteKey; - } - - if (ps.recaptchaSecretKey !== undefined) { - set.recaptchaSecretKey = ps.recaptchaSecretKey; - } - - if (ps.sensitiveMediaDetection !== undefined) { - set.sensitiveMediaDetection = ps.sensitiveMediaDetection; - } - - if (ps.sensitiveMediaDetectionSensitivity !== undefined) { - set.sensitiveMediaDetectionSensitivity = ps.sensitiveMediaDetectionSensitivity; - } + if (Array.isArray(ps.pinnedUsers)) { + set.pinnedUsers = ps.pinnedUsers.filter(Boolean); + } - if (ps.setSensitiveFlagAutomatically !== undefined) { - set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically; - } + if (Array.isArray(ps.hiddenTags)) { + set.hiddenTags = ps.hiddenTags.filter(Boolean); + } - if (ps.enableSensitiveMediaDetectionForVideos !== undefined) { - set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; - } + if (Array.isArray(ps.blockedHosts)) { + set.blockedHosts = ps.blockedHosts.filter(Boolean); + } - if (ps.proxyAccountId !== undefined) { - set.proxyAccountId = ps.proxyAccountId; - } + if (ps.themeColor !== undefined) { + set.themeColor = ps.themeColor; + } - if (ps.maintainerName !== undefined) { - set.maintainerName = ps.maintainerName; - } + if (ps.mascotImageUrl !== undefined) { + set.mascotImageUrl = ps.mascotImageUrl; + } - if (ps.maintainerEmail !== undefined) { - set.maintainerEmail = ps.maintainerEmail; - } + if (ps.bannerUrl !== undefined) { + set.bannerUrl = ps.bannerUrl; + } - if (Array.isArray(ps.langs)) { - set.langs = ps.langs.filter(Boolean); - } + if (ps.iconUrl !== undefined) { + set.iconUrl = ps.iconUrl; + } - if (Array.isArray(ps.pinnedPages)) { - set.pinnedPages = ps.pinnedPages.filter(Boolean); - } + if (ps.backgroundImageUrl !== undefined) { + set.backgroundImageUrl = ps.backgroundImageUrl; + } - if (ps.pinnedClipId !== undefined) { - set.pinnedClipId = ps.pinnedClipId; - } + if (ps.logoImageUrl !== undefined) { + set.logoImageUrl = ps.logoImageUrl; + } - if (ps.summalyProxy !== undefined) { - set.summalyProxy = ps.summalyProxy; - } + if (ps.name !== undefined) { + set.name = ps.name; + } - if (ps.enableTwitterIntegration !== undefined) { - set.enableTwitterIntegration = ps.enableTwitterIntegration; - } + if (ps.description !== undefined) { + set.description = ps.description; + } - if (ps.twitterConsumerKey !== undefined) { - set.twitterConsumerKey = ps.twitterConsumerKey; - } + if (ps.defaultLightTheme !== undefined) { + set.defaultLightTheme = ps.defaultLightTheme; + } - if (ps.twitterConsumerSecret !== undefined) { - set.twitterConsumerSecret = ps.twitterConsumerSecret; - } + if (ps.defaultDarkTheme !== undefined) { + set.defaultDarkTheme = ps.defaultDarkTheme; + } - if (ps.enableGithubIntegration !== undefined) { - set.enableGithubIntegration = ps.enableGithubIntegration; - } + if (ps.localDriveCapacityMb !== undefined) { + set.localDriveCapacityMb = ps.localDriveCapacityMb; + } - if (ps.githubClientId !== undefined) { - set.githubClientId = ps.githubClientId; - } + if (ps.remoteDriveCapacityMb !== undefined) { + set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; + } - if (ps.githubClientSecret !== undefined) { - set.githubClientSecret = ps.githubClientSecret; - } + if (ps.cacheRemoteFiles !== undefined) { + set.cacheRemoteFiles = ps.cacheRemoteFiles; + } - if (ps.enableDiscordIntegration !== undefined) { - set.enableDiscordIntegration = ps.enableDiscordIntegration; - } + if (ps.emailRequiredForSignup !== undefined) { + set.emailRequiredForSignup = ps.emailRequiredForSignup; + } - if (ps.discordClientId !== undefined) { - set.discordClientId = ps.discordClientId; - } + if (ps.enableHcaptcha !== undefined) { + set.enableHcaptcha = ps.enableHcaptcha; + } - if (ps.discordClientSecret !== undefined) { - set.discordClientSecret = ps.discordClientSecret; - } + if (ps.hcaptchaSiteKey !== undefined) { + set.hcaptchaSiteKey = ps.hcaptchaSiteKey; + } - if (ps.enableEmail !== undefined) { - set.enableEmail = ps.enableEmail; - } + if (ps.hcaptchaSecretKey !== undefined) { + set.hcaptchaSecretKey = ps.hcaptchaSecretKey; + } - if (ps.email !== undefined) { - set.email = ps.email; - } + if (ps.enableRecaptcha !== undefined) { + set.enableRecaptcha = ps.enableRecaptcha; + } - if (ps.smtpSecure !== undefined) { - set.smtpSecure = ps.smtpSecure; - } + if (ps.recaptchaSiteKey !== undefined) { + set.recaptchaSiteKey = ps.recaptchaSiteKey; + } - if (ps.smtpHost !== undefined) { - set.smtpHost = ps.smtpHost; - } + if (ps.recaptchaSecretKey !== undefined) { + set.recaptchaSecretKey = ps.recaptchaSecretKey; + } - if (ps.smtpPort !== undefined) { - set.smtpPort = ps.smtpPort; - } + if (ps.sensitiveMediaDetection !== undefined) { + set.sensitiveMediaDetection = ps.sensitiveMediaDetection; + } - if (ps.smtpUser !== undefined) { - set.smtpUser = ps.smtpUser; - } + if (ps.sensitiveMediaDetectionSensitivity !== undefined) { + set.sensitiveMediaDetectionSensitivity = ps.sensitiveMediaDetectionSensitivity; + } - if (ps.smtpPass !== undefined) { - set.smtpPass = ps.smtpPass; - } + if (ps.setSensitiveFlagAutomatically !== undefined) { + set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically; + } - if (ps.errorImageUrl !== undefined) { - set.errorImageUrl = ps.errorImageUrl; - } + if (ps.enableSensitiveMediaDetectionForVideos !== undefined) { + set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; + } - if (ps.enableServiceWorker !== undefined) { - set.enableServiceWorker = ps.enableServiceWorker; - } + if (ps.proxyAccountId !== undefined) { + set.proxyAccountId = ps.proxyAccountId; + } - if (ps.swPublicKey !== undefined) { - set.swPublicKey = ps.swPublicKey; - } + if (ps.maintainerName !== undefined) { + set.maintainerName = ps.maintainerName; + } - if (ps.swPrivateKey !== undefined) { - set.swPrivateKey = ps.swPrivateKey; - } + if (ps.maintainerEmail !== undefined) { + set.maintainerEmail = ps.maintainerEmail; + } - if (ps.tosUrl !== undefined) { - set.ToSUrl = ps.tosUrl; - } + if (Array.isArray(ps.langs)) { + set.langs = ps.langs.filter(Boolean); + } - if (ps.repositoryUrl !== undefined) { - set.repositoryUrl = ps.repositoryUrl; - } + if (Array.isArray(ps.pinnedPages)) { + set.pinnedPages = ps.pinnedPages.filter(Boolean); + } - if (ps.feedbackUrl !== undefined) { - set.feedbackUrl = ps.feedbackUrl; - } + if (ps.pinnedClipId !== undefined) { + set.pinnedClipId = ps.pinnedClipId; + } - if (ps.useObjectStorage !== undefined) { - set.useObjectStorage = ps.useObjectStorage; - } + if (ps.summalyProxy !== undefined) { + set.summalyProxy = ps.summalyProxy; + } - if (ps.objectStorageBaseUrl !== undefined) { - set.objectStorageBaseUrl = ps.objectStorageBaseUrl; - } + if (ps.enableTwitterIntegration !== undefined) { + set.enableTwitterIntegration = ps.enableTwitterIntegration; + } - if (ps.objectStorageBucket !== undefined) { - set.objectStorageBucket = ps.objectStorageBucket; - } + if (ps.twitterConsumerKey !== undefined) { + set.twitterConsumerKey = ps.twitterConsumerKey; + } - if (ps.objectStoragePrefix !== undefined) { - set.objectStoragePrefix = ps.objectStoragePrefix; - } + if (ps.twitterConsumerSecret !== undefined) { + set.twitterConsumerSecret = ps.twitterConsumerSecret; + } - if (ps.objectStorageEndpoint !== undefined) { - set.objectStorageEndpoint = ps.objectStorageEndpoint; - } + if (ps.enableGithubIntegration !== undefined) { + set.enableGithubIntegration = ps.enableGithubIntegration; + } - if (ps.objectStorageRegion !== undefined) { - set.objectStorageRegion = ps.objectStorageRegion; - } + if (ps.githubClientId !== undefined) { + set.githubClientId = ps.githubClientId; + } - if (ps.objectStoragePort !== undefined) { - set.objectStoragePort = ps.objectStoragePort; - } + if (ps.githubClientSecret !== undefined) { + set.githubClientSecret = ps.githubClientSecret; + } - if (ps.objectStorageAccessKey !== undefined) { - set.objectStorageAccessKey = ps.objectStorageAccessKey; - } + if (ps.enableDiscordIntegration !== undefined) { + set.enableDiscordIntegration = ps.enableDiscordIntegration; + } - if (ps.objectStorageSecretKey !== undefined) { - set.objectStorageSecretKey = ps.objectStorageSecretKey; - } + if (ps.discordClientId !== undefined) { + set.discordClientId = ps.discordClientId; + } - if (ps.objectStorageUseSSL !== undefined) { - set.objectStorageUseSSL = ps.objectStorageUseSSL; - } + if (ps.discordClientSecret !== undefined) { + set.discordClientSecret = ps.discordClientSecret; + } - if (ps.objectStorageUseProxy !== undefined) { - set.objectStorageUseProxy = ps.objectStorageUseProxy; - } + if (ps.enableEmail !== undefined) { + set.enableEmail = ps.enableEmail; + } - if (ps.objectStorageSetPublicRead !== undefined) { - set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead; - } + if (ps.email !== undefined) { + set.email = ps.email; + } - if (ps.objectStorageS3ForcePathStyle !== undefined) { - set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; - } + if (ps.smtpSecure !== undefined) { + set.smtpSecure = ps.smtpSecure; + } - if (ps.deeplAuthKey !== undefined) { - if (ps.deeplAuthKey === '') { - set.deeplAuthKey = null; - } else { - set.deeplAuthKey = ps.deeplAuthKey; - } - } + if (ps.smtpHost !== undefined) { + set.smtpHost = ps.smtpHost; + } - if (ps.deeplIsPro !== undefined) { - set.deeplIsPro = ps.deeplIsPro; - } + if (ps.smtpPort !== undefined) { + set.smtpPort = ps.smtpPort; + } - if (ps.enableIpLogging !== undefined) { - set.enableIpLogging = ps.enableIpLogging; - } + if (ps.smtpUser !== undefined) { + set.smtpUser = ps.smtpUser; + } - if (ps.enableActiveEmailValidation !== undefined) { - set.enableActiveEmailValidation = ps.enableActiveEmailValidation; - } + if (ps.smtpPass !== undefined) { + set.smtpPass = ps.smtpPass; + } - await db.transaction(async transactionalEntityManager => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: 'DESC', - }, + if (ps.errorImageUrl !== undefined) { + set.errorImageUrl = ps.errorImageUrl; + } + + if (ps.enableServiceWorker !== undefined) { + set.enableServiceWorker = ps.enableServiceWorker; + } + + if (ps.swPublicKey !== undefined) { + set.swPublicKey = ps.swPublicKey; + } + + if (ps.swPrivateKey !== undefined) { + set.swPrivateKey = ps.swPrivateKey; + } + + if (ps.tosUrl !== undefined) { + set.ToSUrl = ps.tosUrl; + } + + if (ps.repositoryUrl !== undefined) { + set.repositoryUrl = ps.repositoryUrl; + } + + if (ps.feedbackUrl !== undefined) { + set.feedbackUrl = ps.feedbackUrl; + } + + if (ps.useObjectStorage !== undefined) { + set.useObjectStorage = ps.useObjectStorage; + } + + if (ps.objectStorageBaseUrl !== undefined) { + set.objectStorageBaseUrl = ps.objectStorageBaseUrl; + } + + if (ps.objectStorageBucket !== undefined) { + set.objectStorageBucket = ps.objectStorageBucket; + } + + if (ps.objectStoragePrefix !== undefined) { + set.objectStoragePrefix = ps.objectStoragePrefix; + } + + if (ps.objectStorageEndpoint !== undefined) { + set.objectStorageEndpoint = ps.objectStorageEndpoint; + } + + if (ps.objectStorageRegion !== undefined) { + set.objectStorageRegion = ps.objectStorageRegion; + } + + if (ps.objectStoragePort !== undefined) { + set.objectStoragePort = ps.objectStoragePort; + } + + if (ps.objectStorageAccessKey !== undefined) { + set.objectStorageAccessKey = ps.objectStorageAccessKey; + } + + if (ps.objectStorageSecretKey !== undefined) { + set.objectStorageSecretKey = ps.objectStorageSecretKey; + } + + if (ps.objectStorageUseSSL !== undefined) { + set.objectStorageUseSSL = ps.objectStorageUseSSL; + } + + if (ps.objectStorageUseProxy !== undefined) { + set.objectStorageUseProxy = ps.objectStorageUseProxy; + } + + if (ps.objectStorageSetPublicRead !== undefined) { + set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead; + } + + if (ps.objectStorageS3ForcePathStyle !== undefined) { + set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; + } + + if (ps.deeplAuthKey !== undefined) { + if (ps.deeplAuthKey === '') { + set.deeplAuthKey = null; + } else { + set.deeplAuthKey = ps.deeplAuthKey; + } + } + + if (ps.deeplIsPro !== undefined) { + set.deeplIsPro = ps.deeplIsPro; + } + + if (ps.enableIpLogging !== undefined) { + set.enableIpLogging = ps.enableIpLogging; + } + + if (ps.enableActiveEmailValidation !== undefined) { + set.enableActiveEmailValidation = ps.enableActiveEmailValidation; + } + + await this.db.transaction(async transactionalEntityManager => { + const metas = await transactionalEntityManager.find(Meta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + await transactionalEntityManager.update(Meta, meta.id, set); + } else { + await transactionalEntityManager.save(Meta, set); + } + }); + + this.moderationLogService.insertModerationLog(me, 'updateMeta'); }); - - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } - }); - - insertModerationLog(me, 'updateMeta'); -}); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index fa21ab7833..1ea0e6aac4 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -1,5 +1,7 @@ -import { UserProfiles, Users } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], @@ -18,14 +20,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new Error('user not found'); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + 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 UserProfiles.update({ userId: user.id }, { - moderationNote: ps.text, - }); -}); + await this.userProfilesRepository.update({ userId: user.id }, { + moderationNote: ps.text, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/vacuum.ts b/packages/backend/src/server/api/endpoints/admin/vacuum.ts deleted file mode 100644 index 0546acfacb..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/vacuum.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from '../../define.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { db } from '@/db/postgre.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - full: { type: 'boolean' }, - analyze: { type: 'boolean' }, - }, - required: ['full', 'analyze'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const params: string[] = []; - - if (ps.full) { - params.push('FULL'); - } - - if (ps.analyze) { - params.push('ANALYZE'); - } - - db.query('VACUUM ' + params.join(' ')); - - insertModerationLog(me, 'vacuum', ps); -}); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 23cb93c9a5..aa44dfd5dc 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,6 +1,8 @@ -import { Announcements, AnnouncementReads } from '@/models/index.js'; -import define from '../define.js'; -import { makePaginationQuery } from '../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models'; export const meta = { tags: ['meta'], @@ -63,24 +65,37 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, - const announcements = await query.take(ps.limit).getMany(); + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, - if (user) { - const reads = (await AnnouncementReads.findBy({ - userId: user.id, - })).map(x => x.announcementId); + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - for (const announcement of announcements) { - (announcement as any).isRead = reads.includes(announcement.id); - } - } + const announcements = await query.take(ps.limit).getMany(); + + if (me) { + const reads = (await this.announcementReadsRepository.findBy({ + userId: me.id, + })).map(x => x.announcementId); - return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({ - ...a, - createdAt: a.createdAt.toISOString(), - updatedAt: a.updatedAt?.toISOString() ?? null, - })); -}); + for (const announcement of announcements) { + (announcement as any).isRead = reads.includes(announcement.id); + } + } + + return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({ + ...a, + createdAt: a.createdAt.toISOString(), + updatedAt: a.updatedAt?.toISOString() ?? null, + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 7a4923b944..56bd343d55 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,8 +1,11 @@ -import define from '../../define.js'; -import { genId } from '@/misc/gen-id.js'; -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { UserListsRepository, UserGroupJoiningsRepository, AntennasRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['antennas'], @@ -61,48 +64,66 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let userList; - let userGroupJoining; - - if (ps.src === 'list' && ps.userListId) { - userList = await UserLists.findOneBy({ - id: ps.userListId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOneBy({ - userGroupId: ps.userGroupId, - userId: user.id, - }); + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } - } + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private antennaEntityService: AntennaEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + let userList; + let userGroupJoining; + + if (ps.src === 'list' && ps.userListId) { + userList = await this.userListsRepository.findOneBy({ + id: ps.userListId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } else if (ps.src === 'group' && ps.userGroupId) { + userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({ + userGroupId: ps.userGroupId, + userId: me.id, + }); - const antenna = await Antennas.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }).then(x => Antennas.findOneByOrFail(x.identifiers[0])); - - publishInternalEvent('antennaCreated', antenna); - - return await Antennas.pack(antenna); -}); + if (userGroupJoining == null) { + throw new ApiError(meta.errors.noSuchUserGroup); + } + } + + const antenna = await this.antennasRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, + keywords: ps.keywords, + excludeKeywords: ps.excludeKeywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('antennaCreated', antenna); + + return await this.antennaEntityService.pack(antenna); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index ced34ba313..127aca0c33 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AntennasRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Antennas } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['antennas'], @@ -28,17 +30,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: user.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const antenna = await this.antennasRepository.findOneBy({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + await this.antennasRepository.delete(antenna.id); + + this.globalEventService.publishInternalEvent('antennaDeleted', antenna); + }); } - - await Antennas.delete(antenna.id); - - publishInternalEvent('antennaDeleted', antenna); -}); +} diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index c519b452ef..bdc44895cc 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { Antennas } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AntennasRepository } from '@/models/index.js'; +import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['antennas', 'account'], @@ -26,10 +29,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const antennas = await Antennas.findBy({ - userId: me.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, - return await Promise.all(antennas.map(x => Antennas.pack(x))); -}); + private antennaEntityService: AntennaEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const antennas = await this.antennasRepository.findBy({ + userId: me.id, + }); + + return await Promise.all(antennas.map(x => this.antennaEntityService.pack(x))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 8aac55b4a0..eba42afe56 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,11 +1,11 @@ -import define from '../../define.js'; -import readNote from '@/services/note/read.js'; -import { Antennas, Notes, AntennaNotes } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotesRepository, AntennaNotesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { DI } from '@/di-symbols.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { ApiError } from '../../error.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { tags: ['antennas', 'account', 'notes'], @@ -47,43 +47,61 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: user.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .innerJoin(AntennaNotes.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id }); - - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - - const notes = await query - .take(ps.limit) - .getMany(); - - if (notes.length > 0) { - readNote(user.id, notes); - } + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private noteReadService: NoteReadService, + ) { + super(meta, paramDef, async (ps, me) => { + const antenna = await this.antennasRepository.findOneBy({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } - return await Notes.packMany(notes, user); -}); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id }); + + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + + const notes = await query + .take(ps.limit) + .getMany(); + + if (notes.length > 0) { + this.noteReadService.read(me.id, notes); + } + + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index dd693789cb..8bd8ad124d 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AntennasRepository } from '@/models/index.js'; +import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Antennas } from '@/models/index.js'; export const meta = { tags: ['antennas', 'account'], @@ -33,16 +36,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the antenna - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: me.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + private antennaEntityService: AntennaEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the antenna + const antenna = await this.antennasRepository.findOneBy({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + return await this.antennaEntityService.pack(antenna); + }); } - - return await Antennas.pack(antenna); -}); +} diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index edfedc1752..59bba04ee1 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,7 +1,10 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AntennasRepository, UserListsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['antennas'], @@ -67,55 +70,72 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the antenna - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: user.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - let userList; - let userGroupJoining; - - if (ps.src === 'list' && ps.userListId) { - userList = await UserLists.findOneBy({ - id: ps.userListId, - userId: user.id, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private antennaEntityService: AntennaEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the antenna + const antenna = await this.antennasRepository.findOneBy({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + let userList; + let userGroupJoining; + + if (ps.src === 'list' && ps.userListId) { + userList = await this.userListsRepository.findOneBy({ + id: ps.userListId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } else if (ps.src === 'group' && ps.userGroupId) { + userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({ + userGroupId: ps.userGroupId, + userId: me.id, + }); + + if (userGroupJoining == null) { + throw new ApiError(meta.errors.noSuchUserGroup); + } + } + + await this.antennasRepository.update(antenna.id, { + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, + keywords: ps.keywords, + excludeKeywords: ps.excludeKeywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }); + + this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id })); + + return await this.antennaEntityService.pack(antenna.id); }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOneBy({ - userGroupId: ps.userGroupId, - userId: user.id, - }); - - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } } - - await Antennas.update(antenna.id, { - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }); - - publishInternalEvent('antennaUpdated', await Antennas.findOneByOrFail({ id: antenna.id })); - - return await Antennas.pack(antenna.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 0cbe7ebc67..3d4c85e50b 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -1,7 +1,8 @@ -import define from '../../define.js'; -import Resolver from '@/remote/activitypub/resolver.js'; -import { ApiError } from '../../error.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ApResolverService } from '@/core/remote/activitypub/ApResolverService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['federation'], @@ -31,8 +32,15 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const resolver = new Resolver(); - const object = await resolver.resolve(ps.uri); - return object; -}); +@Injectable() +export default class extends Endpoint { + constructor( + private apResolverService: ApResolverService, + ) { + super(meta, paramDef, async (ps, me) => { + const resolver = this.apResolverService.createResolver(); + const object = await resolver.resolve(ps.uri); + return object; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 6442a1412c..eaf93ee977 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -1,18 +1,21 @@ -import define from '../../define.js'; -import config from '@/config/index.js'; -import { createPerson } from '@/remote/activitypub/models/person.js'; -import { createNote } from '@/remote/activitypub/models/note.js'; -import DbResolver from '@/remote/activitypub/db-resolver.js'; -import Resolver from '@/remote/activitypub/resolver.js'; -import { ApiError } from '../../error.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { Users, Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { isActor, isPost, getApId } from '@/remote/activitypub/type.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import { SchemaType } from '@/misc/schema.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, NotesRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { CacheableLocalUser, User } from '@/models/entities/User.js'; +import { isActor, isPost, getApId } from '@/core/remote/activitypub/type.js'; +import type { SchemaType } from '@/misc/schema.js'; +import { ApResolverService } from '@/core/remote/activitypub/ApResolverService.js'; +import { ApDbResolverService } from '@/core/remote/activitypub/ApDbResolverService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApPersonService } from '@/core/remote/activitypub/models/ApPersonService.js'; +import { ApNoteService } from '@/core/remote/activitypub/models/ApNoteService.js'; +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 { ApiError } from '../../error.js'; export const meta = { tags: ['federation'], @@ -47,8 +50,8 @@ export const meta = { type: 'object', optional: false, nullable: false, ref: 'UserDetailedNotMe', - } - } + }, + }, }, { type: 'object', @@ -62,9 +65,9 @@ export const meta = { type: 'object', optional: false, nullable: false, ref: 'Note', - } - } - } + }, + }, + }, ], }, } as const; @@ -78,70 +81,88 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const object = await fetchAny(ps.uri, me); - if (object) { - return object; - } else { - throw new ApiError(meta.errors.noSuchObject); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private utilityService: UtilityService, + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private metaService: MetaService, + private apResolverService: ApResolverService, + private apDbResolverService: ApDbResolverService, + private apPersonService: ApPersonService, + private apNoteService: ApNoteService, + ) { + super(meta, paramDef, async (ps, me) => { + const object = await this.#fetchAny(ps.uri, me); + if (object) { + return object; + } else { + throw new ApiError(meta.errors.noSuchObject); + } + }); } -}); -/*** - * URIからUserかNoteを解決する - */ -async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { + /*** + * URIからUserかNoteを解決する + */ + async #fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 - const fetchedMeta = await fetchMeta(); - if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null; - - const dbResolver = new DbResolver(); - - let local = await mergePack(me, ...await Promise.all([ - dbResolver.getUserFromApId(uri), - dbResolver.getNoteFromApId(uri), - ])); - if (local != null) return local; - - // リモートから一旦オブジェクトフェッチ - const resolver = new Resolver(); - const object = await resolver.resolve(uri) as any; - - // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する - // これはDBに存在する可能性があるため再度DB検索 - if (uri !== object.id) { - local = await mergePack(me, ...await Promise.all([ - dbResolver.getUserFromApId(object.id), - dbResolver.getNoteFromApId(object.id), + const fetchedMeta = await this.metaService.fetch(); + if (fetchedMeta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return null; + + let local = await this.#mergePack(me, ...await Promise.all([ + this.apDbResolverService.getUserFromApId(uri), + this.apDbResolverService.getNoteFromApId(uri), ])); if (local != null) return local; - } - return await mergePack( - me, - isActor(object) ? await createPerson(getApId(object)) : null, - isPost(object) ? await createNote(getApId(object), undefined, true) : null, - ); -} + // リモートから一旦オブジェクトフェッチ + const resolver = this.apResolverService.createResolver(); + const object = await resolver.resolve(uri) as any; + + // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する + // これはDBに存在する可能性があるため再度DB検索 + if (uri !== object.id) { + local = await this.#mergePack(me, ...await Promise.all([ + this.apDbResolverService.getUserFromApId(object.id), + this.apDbResolverService.getNoteFromApId(object.id), + ])); + if (local != null) return local; + } -async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { - if (user != null) { - return { - type: 'User', - object: await Users.pack(user, me, { detail: true }), - }; - } else if (note != null) { - try { - const object = await Notes.pack(note, me, { detail: true }); + return await this.#mergePack( + me, + isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, + isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, + ); + } + async #mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { + if (user != null) { return { - type: 'Note', - object, + type: 'User', + object: await this.userEntityService.pack(user, me, { detail: true }), }; - } catch (e) { - return null; + } else if (note != null) { + try { + const object = await this.noteEntityService.pack(note, me, { detail: true }); + + return { + type: 'Note', + object, + }; + } catch (e) { + return null; + } } - } - return null; + return null; + } } diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index a0a7350822..f52d18f7fe 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -1,8 +1,11 @@ -import define from '../../define.js'; -import { Apps } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { unique } from '@/prelude/array.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AppsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { unique } from '@/misc/prelude/array.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['app'], @@ -30,27 +33,38 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Generate secret - const secret = secureRndstr(32, true); - - // for backward compatibility - const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); - - // Create account - const app = await Apps.insert({ - id: genId(), - createdAt: new Date(), - userId: user ? user.id : null, - name: ps.name, - description: ps.description, - permission, - callbackUrl: ps.callbackUrl, - secret: secret, - }).then(x => Apps.findOneByOrFail(x.identifiers[0])); - - return await Apps.pack(app, null, { - detail: true, - includeSecret: true, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private appEntityService: AppEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + // Generate secret + const secret = secureRndstr(32, true); + + // for backward compatibility + const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); + + // Create account + const app = await this.appsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me ? me.id : null, + name: ps.name, + description: ps.description, + permission, + callbackUrl: ps.callbackUrl, + secret: secret, + }).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.appEntityService.pack(app, null, { + detail: true, + includeSecret: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index 451969d971..f94fed5344 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AppsRepository } from '@/models/index.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Apps } from '@/models/index.js'; export const meta = { tags: ['app'], @@ -29,18 +32,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, token) => { - const isSecure = user != null && token == null; - - // Lookup app - const ap = await Apps.findOneBy({ id: ps.appId }); - - if (ap == null) { - throw new ApiError(meta.errors.noSuchApp); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private appEntityService: AppEntityService, + ) { + super(meta, paramDef, async (ps, user, token) => { + const isSecure = user != null && token == null; + + // Lookup app + const ap = await this.appsRepository.findOneBy({ id: ps.appId }); + + if (ap == null) { + throw new ApiError(meta.errors.noSuchApp); + } + + return await this.appEntityService.pack(ap, user, { + detail: true, + includeSecret: isSecure && (ap.userId === user!.id), + }); + }); } - - return await Apps.pack(ap, user, { - detail: true, - includeSecret: isSecure && (ap.userId === user!.id), - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index b5c06792bb..6032b59bef 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -1,9 +1,11 @@ import * as crypto from 'node:crypto'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { AuthSessions, AccessTokens, Apps } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['auth'], @@ -30,49 +32,65 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch token - const session = await AuthSessions - .findOneBy({ token: ps.token }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } + @Inject(DI.authSessionsRepository) + private authSessionsRepository: AuthSessionsRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch token + const session = await this.authSessionsRepository + .findOneBy({ token: ps.token }); + + if (session == null) { + throw new ApiError(meta.errors.noSuchSession); + } + + // Generate access token + const accessToken = secureRndstr(32, true); - // Generate access token - const accessToken = secureRndstr(32, true); - - // Fetch exist access token - const exist = await AccessTokens.findOneBy({ - appId: session.appId, - userId: user.id, - }); - - if (exist == null) { - // Lookup app - const app = await Apps.findOneByOrFail({ id: session.appId }); - - // Generate Hash - const sha256 = crypto.createHash('sha256'); - sha256.update(accessToken + app.secret); - const hash = sha256.digest('hex'); - - const now = new Date(); - - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - appId: session.appId, - userId: user.id, - token: accessToken, - hash: hash, + // Fetch exist access token + const exist = await this.accessTokensRepository.findOneBy({ + appId: session.appId, + userId: me.id, + }); + + if (exist == null) { + // Lookup app + const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); + + // Generate Hash + const sha256 = crypto.createHash('sha256'); + sha256.update(accessToken + app.secret); + const hash = sha256.digest('hex'); + + const now = new Date(); + + // Insert access token doc + await this.accessTokensRepository.insert({ + id: this.idService.genId(), + createdAt: now, + lastUsedAt: now, + appId: session.appId, + userId: me.id, + token: accessToken, + hash: hash, + }); + } + + // Update session + await this.authSessionsRepository.update(session.id, { + userId: me.id, + }); }); } - - // Update session - await AuthSessions.update(session.id, { - userId: user.id, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index 717c3e5086..7f8325dbbd 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -1,9 +1,11 @@ import { v4 as uuid } from 'uuid'; -import config from '@/config/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Apps, AuthSessions } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['auth'], @@ -44,29 +46,45 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy({ - secret: ps.appSecret, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); - } + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + @Inject(DI.authSessionsRepository) + private authSessionsRepository: AuthSessionsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup app + const app = await this.appsRepository.findOneBy({ + secret: ps.appSecret, + }); - // Generate token - const token = uuid(); + if (app == null) { + throw new ApiError(meta.errors.noSuchApp); + } - // Create session token document - const doc = await AuthSessions.insert({ - id: genId(), - createdAt: new Date(), - appId: app.id, - token: token, - }).then(x => AuthSessions.findOneByOrFail(x.identifiers[0])); + // Generate token + const token = uuid(); - return { - token: doc.token, - url: `${config.authUrl}/${doc.token}`, - }; -}); + // Create session token document + const doc = await this.authSessionsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + appId: app.id, + token: token, + }).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0])); + + return { + token: doc.token, + url: `${this.config.authUrl}/${doc.token}`, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 3f3a4d1427..dff4c74340 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AuthSessionsRepository } from '@/models/index.js'; +import { AuthSessionEntityService } from '@/core/entities/AuthSessionEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { AuthSessions } from '@/models/index.js'; export const meta = { tags: ['auth'], @@ -46,15 +49,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Lookup session - const session = await AuthSessions.findOneBy({ - token: ps.token, - }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.authSessionsRepository) + private authSessionsRepository: AuthSessionsRepository, + + private authSessionEntityService: AuthSessionEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup session + const session = await this.authSessionsRepository.findOneBy({ + token: ps.token, + }); - return await AuthSessions.pack(session, user); -}); + if (session == null) { + throw new ApiError(meta.errors.noSuchSession); + } + + return await this.authSessionEntityService.pack(session, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index 89884ed38a..9c9f13f502 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Apps, AuthSessions, AccessTokens, Users } from '@/models/index.js'; export const meta = { tags: ['auth'], @@ -55,43 +58,62 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy({ - secret: ps.appSecret, - }); - - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + @Inject(DI.authSessionsRepository) + private authSessionsRepository: AuthSessionsRepository, + + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup app + const app = await this.appsRepository.findOneBy({ + secret: ps.appSecret, + }); + + if (app == null) { + throw new ApiError(meta.errors.noSuchApp); + } + + // Fetch token + const session = await this.authSessionsRepository.findOneBy({ + token: ps.token, + appId: app.id, + }); + + if (session == null) { + throw new ApiError(meta.errors.noSuchSession); + } + + if (session.userId == null) { + throw new ApiError(meta.errors.pendingSession); + } + + // Lookup access token + const accessToken = await this.accessTokensRepository.findOneByOrFail({ + appId: app.id, + userId: session.userId, + }); + + // Delete session + this.authSessionsRepository.delete(session.id); + + return { + accessToken: accessToken.token, + user: await this.userEntityService.pack(session.userId, null, { + detail: true, + }), + }; + }); } - - // Fetch token - const session = await AuthSessions.findOneBy({ - token: ps.token, - appId: app.id, - }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } - - if (session.userId == null) { - throw new ApiError(meta.errors.pendingSession); - } - - // Lookup access token - const accessToken = await AccessTokens.findOneByOrFail({ - appId: app.id, - userId: session.userId, - }); - - // Delete session - AuthSessions.delete(session.id); - - return { - accessToken: accessToken.token, - user: await Users.pack(session.userId, null, { - detail: true, - }), - }; -}); +} diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 0540e6ab0f..33614a1554 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import create from '@/services/blocking/create.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Blockings, NoteWatchings, Users } from '@/models/index.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account'], @@ -53,38 +56,48 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: user.id }); - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); - } - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already blocking - const exist = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyBlocking); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userBlockingService: UserBlockingService, + ) { + super(meta, paramDef, async (ps, me) => { + const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); + + // 自分自身 + if (me.id === ps.userId) { + throw new ApiError(meta.errors.blockeeIsYourself); + } + + // Get blockee + const blockee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check if already blocking + const exist = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyBlocking); + } + + await this.userBlockingService.block(blocker, blockee); + + return await this.userEntityService.pack(blockee.id, blocker, { + detail: true, + }); + }); } - - await create(blocker, blockee); - - NoteWatchings.delete({ - userId: blocker.id, - noteUserId: blockee.id, - }); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index 77e17b3ba9..f2cc28e922 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import deleteBlocking from '@/services/blocking/delete.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Blockings, Users } from '@/models/index.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account'], @@ -53,34 +56,49 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: user.id }); - - // Check if the blockee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userBlockingService: UserBlockingService, + ) { + super(meta, paramDef, async (ps, me) => { + const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); + + // Check if the blockee is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.blockeeIsYourself); + } + + // Get blockee + const blockee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not blocking + const exist = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notBlocking); + } + + // Delete blocking + await this.userBlockingService.unblock(blocker, blockee); + + return await this.userEntityService.pack(blockee.id, blocker, { + detail: true, + }); + }); } - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not blocking - const exist = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notBlocking); - } - - // Delete blocking - await deleteBlocking(blocker, blockee); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 29095ebe21..4f5e11cd68 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Blockings } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { BlockingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) - .andWhere(`blocking.blockerId = :meId`, { meId: me.id }); - - const blockings = await query - .take(ps.limit) - .getMany(); - - return await Blockings.packMany(blockings, me); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private blockingEntityService: BlockingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) + .andWhere('blocking.blockerId = :meId', { meId: me.id }); + + const blockings = await query + .take(ps.limit) + .getMany(); + + return await this.blockingEntityService.packMany(blockings, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 94dcfe5023..21979884f9 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,8 +1,11 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import { IdService } from '@/core/IdService.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Channels, DriveFiles } from '@/models/index.js'; -import { Channel } from '@/models/entities/channel.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['channels'], @@ -37,27 +40,41 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let banner = null; - if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ - id: ps.bannerId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private idService: IdService, + private channelEntityService: ChannelEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + let banner = null; + if (ps.bannerId != null) { + banner = await this.driveFilesRepository.findOneBy({ + id: ps.bannerId, + userId: me.id, + }); + + if (banner == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } - const channel = await Channels.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - description: ps.description || null, - bannerId: banner ? banner.id : null, - } as Channel).then(x => Channels.findOneByOrFail(x.identifiers[0])); - - return await Channels.pack(channel, user); -}); + const channel = await this.channelsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + description: ps.description ?? null, + bannerId: banner ? banner.id : null, + } as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.channelEntityService.pack(channel, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index 73980c0fad..0c3f9509d1 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { Channels } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelsRepository } from '@/models/index.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['channels'], @@ -24,12 +27,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Channels.createQueryBuilder('channel') - .where('channel.lastNotedAt IS NOT NULL') - .orderBy('channel.lastNotedAt', 'DESC'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, - const channels = await query.take(10).getMany(); + private channelEntityService: ChannelEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.channelsRepository.createQueryBuilder('channel') + .where('channel.lastNotedAt IS NOT NULL') + .orderBy('channel.lastNotedAt', 'DESC'); - return await Promise.all(channels.map(x => Channels.pack(x, me))); -}); + const channels = await query.take(10).getMany(); + + return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 895ffed0bd..6c6b498a94 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { publishUserEvent } from '@/services/stream.js'; export const meta = { tags: ['channels'], @@ -29,21 +31,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const channel = await Channels.findOneBy({ + id: ps.channelId, + }); - await ChannelFollowings.insert({ - id: genId(), - createdAt: new Date(), - followerId: user.id, - followeeId: channel.id, - }); + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } - publishUserEvent(user.id, 'followChannel', channel); -}); + await this.channelFollowingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + followerId: me.id, + followeeId: channel.id, + }); + + this.globalEventService.publishUserEvent(me.id, 'followChannel', channel); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index e4aa4d1614..5a8ab26af9 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelFollowingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['channels', 'account'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(ChannelFollowings.createQueryBuilder(), ps.sinceId, ps.untilId) - .andWhere({ followerId: me.id }); - - const followings = await query - .take(ps.limit) - .getMany(); - - return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + private channelEntityService: ChannelEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId) + .andWhere({ followerId: me.id }); + + const followings = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index ed7e41cac2..8b8b5819e6 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Channels } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['channels', 'account'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Channels.createQueryBuilder(), ps.sinceId, ps.untilId) - .andWhere({ userId: me.id }); - - const channels = await query - .take(ps.limit) - .getMany(); - - return await Promise.all(channels.map(x => Channels.pack(x, me))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private channelEntityService: ChannelEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder(), ps.sinceId, ps.untilId) + .andWhere({ userId: me.id }); + + const channels = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 87665a9865..54ae31790b 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelsRepository } from '@/models/index.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Channels } from '@/models/index.js'; export const meta = { tags: ['channels'], @@ -31,14 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private channelEntityService: ChannelEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const channel = await this.channelsRepository.findOneBy({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + return await this.channelEntityService.pack(channel, me); + }); } - - return await Channels.pack(channel, me); -}); +} diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index deaa299013..1c7f1360b9 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,8 +1,11 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelsRepository, NotesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Notes, Channels } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; export const meta = { tags: ['notes', 'channels'], @@ -42,35 +45,50 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + const channel = await this.channelsRepository.findOneBy({ + id: ps.channelId, + }); - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .leftJoinAndSelect('note.channel', 'channel'); - //#endregion + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } - const timeline = await query.take(ps.limit).getMany(); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.channelId = :channelId', { channelId: channel.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .leftJoinAndSelect('note.channel', 'channel'); + //#endregion - if (user) activeUsersChart.read(user); + const timeline = await query.take(ps.limit).getMany(); - return await Notes.packMany(timeline, user); -}); + if (me) this.activeUsersChart.read(me); + + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index e065d897a5..b464c55097 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; export const meta = { tags: ['channels'], @@ -28,19 +30,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const channel = await this.channelsRepository.findOneBy({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + await this.channelFollowingsRepository.delete({ + followerId: me.id, + followeeId: channel.id, + }); + + this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel); + }); } - - await ChannelFollowings.delete({ - followerId: user.id, - followeeId: channel.id, - }); - - publishUserEvent(user.id, 'unfollowChannel', channel); -}); +} diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 13104f324f..ba62e9d371 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Channels, DriveFiles } from '@/models/index.js'; export const meta = { tags: ['channels'], @@ -48,39 +51,52 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (channel.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } + private channelEntityService: ChannelEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const channel = await this.channelsRepository.findOneBy({ + id: ps.channelId, + }); - // eslint:disable-next-line:no-unnecessary-initializer - let banner = undefined; - if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ - id: ps.bannerId, - userId: me.id, - }); + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } else if (ps.bannerId === null) { - banner = null; - } + if (channel.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } - await Channels.update(channel.id, { - ...(ps.name !== undefined ? { name: ps.name } : {}), - ...(ps.description !== undefined ? { description: ps.description } : {}), - ...(banner ? { bannerId: banner.id } : {}), - }); + // eslint:disable-next-line:no-unnecessary-initializer + let banner = undefined; + if (ps.bannerId != null) { + banner = await this.driveFilesRepository.findOneBy({ + id: ps.bannerId, + userId: me.id, + }); - return await Channels.pack(channel.id, me); -}); + if (banner == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } else if (ps.bannerId === null) { + banner = null; + } + + await this.channelsRepository.update(channel.id, { + ...(ps.name !== undefined ? { name: ps.name } : {}), + ...(ps.description !== undefined ? { description: ps.description } : {}), + ...(banner ? { bannerId: banner.id } : {}), + }); + + return await this.channelEntityService.pack(channel.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index ea23794296..862ef89268 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { schema } from '@/core/chart/charts/entities/active-users.js'; export const meta = { tags: ['charts', 'users'], - res: getJsonSchema(activeUsersChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await activeUsersChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.activeUsersChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index 06dee250ee..1d5b8f05f8 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { apRequestChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import ApRequestChart from '@/core/chart/charts/ap-request.js'; +import { schema } from '@/core/chart/charts/entities/ap-request.js'; export const meta = { tags: ['charts'], - res: getJsonSchema(apRequestChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await apRequestChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private apRequestChart: ApRequestChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.apRequestChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index dd2c2d6838..ec28fa75de 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { driveChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import DriveChart from '@/core/chart/charts/drive.js'; +import { schema } from '@/core/chart/charts/entities/drive.js'; export const meta = { tags: ['charts', 'drive'], - res: getJsonSchema(driveChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await driveChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private driveChart: DriveChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.driveChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index 8c35b3c46d..6c24cbbb77 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { federationChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import FederationChart from '@/core/chart/charts/federation.js'; +import { schema } from '@/core/chart/charts/entities/federation.js'; export const meta = { tags: ['charts'], - res: getJsonSchema(federationChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await federationChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private federationChart: FederationChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.federationChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts index 77e24a62c3..71e5bab766 100644 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { hashtagChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import HashtagChart from '@/core/chart/charts/hashtag.js'; +import { schema } from '@/core/chart/charts/entities/hashtag.js'; export const meta = { tags: ['charts', 'hashtags'], - res: getJsonSchema(hashtagChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private hashtagChart: HashtagChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 817d51ad01..a6a538ea5c 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { instanceChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import { schema } from '@/core/chart/charts/entities/instance.js'; export const meta = { tags: ['charts'], - res: getJsonSchema(instanceChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await instanceChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.host); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private instanceChart: InstanceChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.instanceChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.host); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index 951adf5408..8d03f2eaf1 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { notesChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import { schema } from '@/core/chart/charts/entities/notes.js'; export const meta = { tags: ['charts', 'notes'], - res: getJsonSchema(notesChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await notesChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private notesChart: NotesChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.notesChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index f165b40224..87d56f38b7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { perUserDriveChart } from '@/services/chart/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; +import { schema } from '@/core/chart/charts/entities/per-user-drive.js'; export const meta = { tags: ['charts', 'drive', 'users'], - res: getJsonSchema(perUserDriveChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await perUserDriveChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private perUserDriveChart: PerUserDriveChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.perUserDriveChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index f5d42e21c2..7a61544aea 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -1,11 +1,13 @@ -import define from '../../../define.js'; -import { getJsonSchema } from '@/services/chart/core.js'; -import { perUserFollowingChart } from '@/services/chart/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { getJsonSchema } from '@/core/chart/core.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; +import { schema } from '@/core/chart/charts/entities/per-user-following.js'; export const meta = { tags: ['charts', 'users', 'following'], - res: getJsonSchema(perUserFollowingChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private perUserFollowingChart: PerUserFollowingChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index aefe550d43..fdc385191f 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { perUserNotesChart } from '@/services/chart/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; +import { schema } from '@/core/chart/charts/entities/per-user-notes.js'; export const meta = { tags: ['charts', 'users', 'notes'], - res: getJsonSchema(perUserNotesChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await perUserNotesChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private perUserNotesChart: PerUserNotesChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.perUserNotesChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 6bc6b56bf0..f0f3e520da 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { perUserReactionsChart } from '@/services/chart/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; +import { schema } from '@/core/chart/charts/entities/per-user-reactions.js'; export const meta = { tags: ['charts', 'users', 'reactions'], - res: getJsonSchema(perUserReactionsChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -23,6 +25,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await perUserReactionsChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private perUserReactionsChart: PerUserReactionsChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.perUserReactionsChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 338e8fd338..d09f2512e5 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,11 +1,13 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { usersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { getJsonSchema } from '@/core/chart/core.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import { schema } from '@/core/chart/charts/entities/users.js'; export const meta = { tags: ['charts', 'users'], - res: getJsonSchema(usersChart.schema), + res: getJsonSchema(schema), allowGet: true, cacheSec: 60 * 60, @@ -22,6 +24,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await usersChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private usersChart: UsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.usersChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index 5d72f5c1bf..c733d28657 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { ClipNotes, Clips } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { ApiError } from '../../error.js'; -import { genId } from '@/misc/gen-id.js'; -import { getNote } from '../../common/getters.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -42,33 +44,47 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); + private idService: IdService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); - const exist = await ClipNotes.findOneBy({ - noteId: note.id, - clipId: clip.id, - }); + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } - if (exist != null) { - throw new ApiError(meta.errors.alreadyClipped); - } + const note = await this.getterService.getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + const exist = await this.clipNotesRepository.findOneBy({ + noteId: note.id, + clipId: clip.id, + }); - await ClipNotes.insert({ - id: genId(), - noteId: note.id, - clipId: clip.id, - }); -}); + if (exist != null) { + throw new ApiError(meta.errors.alreadyClipped); + } + + await this.clipNotesRepository.insert({ + id: this.idService.genId(), + noteId: note.id, + clipId: clip.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index 4afe4222a1..8eca3d66d1 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { genId } from '@/misc/gen-id.js'; -import { Clips } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { ClipsRepository } from '@/models/index.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['clips'], @@ -27,15 +30,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - isPublic: ps.isPublic, - description: ps.description, - }).then(x => Clips.findOneByOrFail(x.identifiers[0])); - - return await Clips.pack(clip); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + private clipEntityService: ClipEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + isPublic: ps.isPublic, + description: ps.description, + }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.clipEntityService.pack(clip); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index b6c0eb702a..ea361ae9c0 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -1,6 +1,8 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Clips } from '@/models/index.js'; export const meta = { tags: ['clips'], @@ -27,15 +29,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + await this.clipsRepository.delete(clip.id); + }); } - - await Clips.delete(clip.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 378811eba0..b57affd1c4 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { Clips } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipsRepository } from '@/models/index.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['clips', 'account'], @@ -26,10 +29,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const clips = await Clips.findBy({ - userId: me.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, - return await Promise.all(clips.map(x => Clips.pack(x))); -}); + private clipEntityService: ClipEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const clips = await this.clipsRepository.findBy({ + userId: me.id, + }); + + return await Promise.all(clips.map(x => this.clipEntityService.pack(x))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 4ace747efe..4282498931 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -1,10 +1,10 @@ -import define from '../../define.js'; -import { ClipNotes, Clips, Notes } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -44,43 +44,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - if (!clip.isPublic && (user == null || (clip.userId !== user.id))) { - throw new ApiError(meta.errors.noSuchClip); - } + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(ClipNotes.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); - - if (user) { - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - } + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } - const notes = await query - .take(ps.limit) - .getMany(); + if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { + throw new ApiError(meta.errors.noSuchClip); + } - return await Notes.packMany(notes, user); -}); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); + + if (me) { + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + + const notes = await query + .take(ps.limit) + .getMany(); + + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 8b90e31f65..3fc60e3639 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { ClipNotes, Clips } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getNote } from '../../common/getters.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -35,23 +37,36 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } - await ClipNotes.delete({ - noteId: note.id, - clipId: clip.id, - }); -}); + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + await this.clipNotesRepository.delete({ + noteId: note.id, + clipId: clip.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index c3d73c168d..4e93540054 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipsRepository } from '@/models/index.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Clips } from '@/models/index.js'; export const meta = { tags: ['clips', 'account'], @@ -33,19 +36,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the clip - const clip = await Clips.findOneBy({ - id: ps.clipId, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, - if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { - throw new ApiError(meta.errors.noSuchClip); - } + private clipEntityService: ClipEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the clip + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } - return await Clips.pack(clip); -}); + if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { + throw new ApiError(meta.errors.noSuchClip); + } + + return await this.clipEntityService.pack(clip); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index b67d844f6e..9880505d06 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipsRepository } from '@/models/index.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { Clips } from '@/models/index.js'; export const meta = { tags: ['clips'], @@ -36,22 +39,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the clip - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + private clipEntityService: ClipEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the clip + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); - await Clips.update(clip.id, { - name: ps.name, - description: ps.description, - isPublic: ps.isPublic, - }); + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } - return await Clips.pack(clip.id); -}); + await this.clipsRepository.update(clip.id, { + name: ps.name, + description: ps.description, + isPublic: ps.isPublic, + }); + + return await this.clipEntityService.pack(clip.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 82497adefa..6f40225f15 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,6 +1,7 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { DriveFiles } from '@/models/index.js'; -import define from '../define.js'; +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'; export const meta = { tags: ['drive', 'account'], @@ -32,14 +33,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(true); - - // Calculate drive usage - const usage = await DriveFiles.calcDriveUsageOf(user.id); - - return { - capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb), - usage: usage, - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + private metaService: MetaService, + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.metaService.fetch(true); + + // Calculate drive usage + const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); + + return { + capacity: 1024 * 1024 * (me.driveCapacityOverrideMb ?? instance.localDriveCapacityMb), + usage: usage, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 40e6c16c9c..56055d1340 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { DriveFiles } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -33,25 +36,36 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) - .andWhere('file.userId = :userId', { userId: user.id }); - - if (ps.folderId) { - query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); - } else { - query.andWhere('file.folderId IS NULL'); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } + private driveFileEntityService: DriveFileEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: me.id }); + + if (ps.folderId) { + query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); + } else { + query.andWhere('file.folderId IS NULL'); + } - const files = await query.take(ps.limit).getMany(); + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); + const files = await query.take(ps.limit).getMany(); + + return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 415a8cc693..9f11eb8b53 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles, Notes } from '@/models/index.js'; export const meta = { tags: ['drive', 'notes'], @@ -39,22 +42,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch file - const file = await DriveFiles.findOneBy({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch file + const file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: me.id, + }); - const notes = await Notes.createQueryBuilder('note') - .where(':file = ANY(note.fileIds)', { file: file.id }) - .getMany(); + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } - return await Notes.packMany(notes, user, { - detail: true, - }); -}); + const notes = await this.notesRepository.createQueryBuilder('note') + .where(':file = ANY(note.fileIds)', { file: file.id }) + .getMany(); + + return await this.noteEntityService.packMany(notes, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index bbae9bf4e4..176031d808 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -25,11 +27,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ - md5: ps.md5, - userId: user.id, - }); - - return file != null; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ + md5: ps.md5, + userId: me.id, + }); + + return file != null; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index ddcbd62889..bff83876d7 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,11 +1,13 @@ import ms from 'ms'; -import { addFile } from '@/services/drive/add-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import define from '../../../define.js'; -import { apiLogger } from '../../../logger.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -64,48 +66,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => { - // Get 'name' parameter - let name = ps.name || file.originalname; - if (name !== undefined && name !== null) { - name = name.trim(); - if (name.length === 0) { - name = null; - } else if (name === 'blob') { - name = null; - } else if (!DriveFiles.validateFileName(name)) { - throw new ApiError(meta.errors.invalidFileName); - } - } else { - name = null; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - const meta = await fetchMeta(); + private driveFileEntityService: DriveFileEntityService, + private metaService: MetaService, + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { + // Get 'name' parameter + let name = ps.name ?? file.originalname; + if (name !== undefined && name !== null) { + name = name.trim(); + if (name.length === 0) { + name = null; + } else if (name === 'blob') { + name = null; + } else if (!this.driveFileEntityService.validateFileName(name)) { + throw new ApiError(meta.errors.invalidFileName); + } + } else { + name = null; + } - try { - // Create file - const driveFile = await addFile({ - user, - path: file.path, - name, - comment: ps.comment, - folderId: ps.folderId, - force: ps.force, - sensitive: ps.isSensitive, - requestIp: meta.enableIpLogging ? ip : null, - requestHeaders: meta.enableIpLogging ? headers : null, - }); - return await DriveFiles.pack(driveFile, { self: true }); - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - apiLogger.error(e); - } - if (e instanceof IdentifiableError) { - if (e.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); - if (e.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); - } - throw new ApiError(); - } finally { + const meta = await this.metaService.fetch(); + + try { + // Create file + const driveFile = await this.driveService.addFile({ + user: me, + path: file.path, + name, + comment: ps.comment, + folderId: ps.folderId, + force: ps.force, + sensitive: ps.isSensitive, + requestIp: meta.enableIpLogging ? ip : null, + requestHeaders: meta.enableIpLogging ? headers : null, + }); + return await this.driveFileEntityService.pack(driveFile, { self: true }); + } catch (err) { + if (err instanceof Error || typeof err === 'string') { + console.error(err); + } + if (err instanceof IdentifiableError) { + if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); + if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); + } + throw new ApiError(); + } finally { cleanup!(); + } + }); } -}); +} 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 6108ae7da9..9d2ea6011a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,8 +1,10 @@ -import { deleteFile } from '@/services/drive/delete-file.js'; -import { publishDriveStream } from '@/services/stream.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DriveService } from '@/core/DriveService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles, Users } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -37,20 +39,31 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + private driveService: DriveService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } - // Delete - await deleteFile(file); + if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } - // Publish fileDeleted event - publishDriveStream(user.id, 'fileDeleted', file.id); -}); + // Delete + await this.driveService.deleteFile(file); + + // Publish fileDeleted event + this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index f2bc7348c6..6299ca8f6b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,8 @@ -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { 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'; export const meta = { tags: ['drive'], @@ -30,11 +33,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = await DriveFiles.findBy({ - md5: ps.md5, - userId: user.id, - }); - - return await DriveFiles.packMany(files, { self: true }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = await this.driveFilesRepository.findBy({ + md5: ps.md5, + userId: me.id, + }); + + return await this.driveFileEntityService.packMany(files, { self: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 245fb45a65..e4cd5213dd 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { DriveFiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -32,12 +35,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = await DriveFiles.findBy({ - name: ps.name, - userId: user.id, - folderId: ps.folderId ?? IsNull(), - }); - - return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true }))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = await this.driveFilesRepository.findBy({ + name: ps.name, + userId: me.id, + folderId: ps.folderId ?? IsNull(), + }); + + return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true }))); + }); + } +} 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 2c604c54c8..bae4d7d66f 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,6 +1,9 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles, Users } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { 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 { ApiError } from '../../../error.js'; export const meta = { @@ -52,34 +55,44 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let file: DriveFile | null = null; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (ps.fileId) { - file = await DriveFiles.findOneBy({ id: ps.fileId }); - } else if (ps.url) { - file = await DriveFiles.findOne({ - where: [{ - url: ps.url, - }, { - webpublicUrl: ps.url, - }, { - thumbnailUrl: ps.url, - }], - }); - } + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + let file: DriveFile | null = null; - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (ps.fileId) { + file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + } else if (ps.url) { + file = await this.driveFilesRepository.findOne({ + where: [{ + url: ps.url, + }, { + webpublicUrl: ps.url, + }, { + thumbnailUrl: ps.url, + }], + }); + } - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } - return await DriveFiles.pack(file, { - detail: true, - withUser: true, - self: true, - }); -}); + if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + return await this.driveFileEntityService.pack(file, { + detail: true, + withUser: true, + self: true, + }); + }); + } +} 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 fa2ec8519c..03e3663f08 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,7 +1,10 @@ -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFiles, DriveFolders, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; -import define from '../../../define.js'; +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 { ApiError } from '../../../error.js'; export const meta = { @@ -59,54 +62,68 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFileEntityService: DriveFileEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } + if (ps.name) file.name = ps.name; + if (!this.driveFileEntityService.validateFileName(file.name)) { + throw new ApiError(meta.errors.invalidFileName); + } - if (ps.name) file.name = ps.name; - if (!DriveFiles.validateFileName(file.name)) { - throw new ApiError(meta.errors.invalidFileName); - } + if (ps.comment !== undefined) file.comment = ps.comment; - if (ps.comment !== undefined) file.comment = ps.comment; + if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; - if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; + if (ps.folderId !== undefined) { + if (ps.folderId === null) { + file.folderId = null; + } else { + const folder = await this.driveFoldersRepository.findOneBy({ + id: ps.folderId, + userId: me.id, + }); - if (ps.folderId !== undefined) { - if (ps.folderId === null) { - file.folderId = null; - } else { - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); + file.folderId = folder.id; + } } - file.folderId = folder.id; - } - } - - await DriveFiles.update(file.id, { - name: file.name, - comment: file.comment, - folderId: file.folderId, - isSensitive: file.isSensitive, - }); + await this.driveFilesRepository.update(file.id, { + name: file.name, + comment: file.comment, + folderId: file.folderId, + isSensitive: file.isSensitive, + }); - const fileObj = await DriveFiles.pack(file, { self: true }); + const fileObj = await this.driveFileEntityService.pack(file, { self: true }); - // Publish fileUpdated event - publishDriveStream(user.id, 'fileUpdated', fileObj); + // Publish fileUpdated event + this.globalEventService.publishDriveStream(me.id, 'fileUpdated', fileObj); - return fileObj; -}); + return fileObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index eb8071c3c9..f4f8df3c2b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { DriveFiles } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; -import define from '../../../define.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -34,13 +37,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { - uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { - DriveFiles.pack(file, { self: true }).then(packedFile => { - publishMainStream(user.id, 'urlUploadFinished', { - marker: ps.marker, - file: packedFile, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveFileEntityService: DriveFileEntityService, + private driveService: DriveService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { + this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { + this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { + this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', { + marker: ps.marker, + file: packedFile, + }); + }); }); }); - }); -}); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index d4d530ba9e..703dc83ecd 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { DriveFolders } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -32,17 +35,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) - .andWhere('folder.userId = :userId', { userId: user.id }); - - if (ps.folderId) { - query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); - } else { - query.andWhere('folder.parentId IS NULL'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFolderEntityService: DriveFolderEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.driveFoldersRepository.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + .andWhere('folder.userId = :userId', { userId: me.id }); + + if (ps.folderId) { + query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); + } else { + query.andWhere('folder.parentId IS NULL'); + } + + const folders = await query.take(ps.limit).getMany(); + + return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder))); + }); } - - const folders = await query.take(ps.limit).getMany(); - - return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); -}); +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 3d7f514c85..7604eaf489 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,8 +1,11 @@ -import { publishDriveStream } from '@/services/stream.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFolders } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['drive'], @@ -29,41 +32,53 @@ export const meta = { export const paramDef = { type: 'object', properties: { - name: { type: 'string', default: "Untitled", maxLength: 200 }, + name: { type: 'string', default: 'Untitled', maxLength: 200 }, parentId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // If the parent folder is specified - let parent = null; - if (ps.parentId) { - // Fetch parent folder - parent = await DriveFolders.findOneBy({ - id: ps.parentId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, - if (parent == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - } + private driveFolderEntityService: DriveFolderEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // If the parent folder is specified + let parent = null; + if (ps.parentId) { + // Fetch parent folder + parent = await this.driveFoldersRepository.findOneBy({ + id: ps.parentId, + userId: me.id, + }); + + if (parent == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + } - // Create folder - const folder = await DriveFolders.insert({ - id: genId(), - createdAt: new Date(), - name: ps.name, - parentId: parent !== null ? parent.id : null, - userId: user.id, - }).then(x => DriveFolders.findOneByOrFail(x.identifiers[0])); + // Create folder + const folder = await this.driveFoldersRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + name: ps.name, + parentId: parent !== null ? parent.id : null, + userId: me.id, + }).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0])); - const folderObj = await DriveFolders.pack(folder); + const folderObj = await this.driveFolderEntityService.pack(folder); - // Publish folderCreated event - publishDriveStream(user.id, 'folderCreated', folderObj); + // Publish folderCreated event + this.globalEventService.publishDriveStream(me.id, 'folderCreated', folderObj); - return folderObj; -}); + return folderObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index ab9d411ec0..dcbaecf8af 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; -import { publishDriveStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository, DriveFilesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFolders, DriveFiles } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -34,28 +36,41 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolders.countBy({ parentId: folder.id }), - DriveFiles.countBy({ folderId: folder.id }), - ]); + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, - if (childFoldersCount !== 0 || childFilesCount !== 0) { - throw new ApiError(meta.errors.hasChildFilesOrFolders); - } + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get folder + const folder = await this.driveFoldersRepository.findOneBy({ + id: ps.folderId, + userId: me.id, + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } - await DriveFolders.delete(folder.id); + const [childFoldersCount, childFilesCount] = await Promise.all([ + this.driveFoldersRepository.countBy({ parentId: folder.id }), + this.driveFilesRepository.countBy({ folderId: folder.id }), + ]); - // Publish folderCreated event - publishDriveStream(user.id, 'folderDeleted', folder.id); -}); + if (childFoldersCount !== 0 || childFilesCount !== 0) { + throw new ApiError(meta.errors.hasChildFilesOrFolders); + } + + await this.driveFoldersRepository.delete(folder.id); + + // Publish folderCreated event + this.globalEventService.publishDriveStream(me.id, 'folderDeleted', folder.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index 1feab273a1..96a87344a9 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { DriveFolders } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository } from '@/models/index.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -30,12 +33,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const folders = await DriveFolders.findBy({ - name: ps.name, - userId: user.id, - parentId: ps.parentId ?? IsNull(), - }); - - return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFolderEntityService: DriveFolderEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const folders = await this.driveFoldersRepository.findBy({ + name: ps.name, + userId: me.id, + parentId: ps.parentId ?? IsNull(), + }); + + return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index 1e7aa2b16c..4c25bc705c 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository } from '@/models/index.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFolders } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -33,18 +36,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFolderEntityService: DriveFolderEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get folder + const folder = await this.driveFoldersRepository.findOneBy({ + id: ps.folderId, + userId: me.id, + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + return await this.driveFolderEntityService.pack(folder, { + detail: true, + }); + }); } - - return await DriveFolders.pack(folder, { - detail: true, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 1aa2e84292..4fcd37bbbf 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,7 +1,10 @@ -import { publishDriveStream } from '@/services/stream.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFoldersRepository } from '@/models/index.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFolders } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -48,71 +51,82 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch folder - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - if (ps.name) folder.name = ps.name; - - if (ps.parentId !== undefined) { - if (ps.parentId === folder.id) { - throw new ApiError(meta.errors.recursiveNesting); - } else if (ps.parentId === null) { - folder.parentId = null; - } else { - // Get parent folder - const parent = await DriveFolders.findOneBy({ - id: ps.parentId, - userId: user.id, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFolderEntityService: DriveFolderEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch folder + const folder = await this.driveFoldersRepository.findOneBy({ + id: ps.folderId, + userId: me.id, }); - if (parent == null) { - throw new ApiError(meta.errors.noSuchParentFolder); + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); } - // Check if the circular reference will occur - async function checkCircle(folderId: string): Promise { - // Fetch folder - const folder2 = await DriveFolders.findOneBy({ - id: folderId, - }); - - if (folder2!.id === folder!.id) { - return true; - } else if (folder2!.parentId) { - return await checkCircle(folder2!.parentId); - } else { - return false; - } - } + if (ps.name) folder.name = ps.name; - if (parent.parentId !== null) { - if (await checkCircle(parent.parentId)) { + if (ps.parentId !== undefined) { + if (ps.parentId === folder.id) { throw new ApiError(meta.errors.recursiveNesting); + } else if (ps.parentId === null) { + folder.parentId = null; + } else { + // Get parent folder + const parent = await this.driveFoldersRepository.findOneBy({ + id: ps.parentId, + userId: me.id, + }); + + if (parent == null) { + throw new ApiError(meta.errors.noSuchParentFolder); + } + + // Check if the circular reference will occur + const checkCircle = async (folderId: string): Promise => { + // Fetch folder + const folder2 = await this.driveFoldersRepository.findOneBy({ + id: folderId, + }); + + if (folder2!.id === folder!.id) { + return true; + } else if (folder2!.parentId) { + return await checkCircle(folder2!.parentId); + } else { + return false; + } + }; + + if (parent.parentId !== null) { + if (await checkCircle(parent.parentId)) { + throw new ApiError(meta.errors.recursiveNesting); + } + } + + folder.parentId = parent.id; } } - folder.parentId = parent.id; - } - } - - // Update - DriveFolders.update(folder.id, { - name: folder.name, - parentId: folder.parentId, - }); + // Update + this.driveFoldersRepository.update(folder.id, { + name: folder.name, + parentId: folder.parentId, + }); - const folderObj = await DriveFolders.pack(folder); + const folderObj = await this.driveFolderEntityService.pack(folder); - // Publish folderUpdated event - publishDriveStream(user.id, 'folderUpdated', folderObj); + // Publish folderUpdated event + this.globalEventService.publishDriveStream(me.id, 'folderUpdated', folderObj); - return folderObj; -}); + return folderObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index 99e8d024fb..aba73c2092 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { DriveFiles } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -32,19 +35,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) - .andWhere('file.userId = :userId', { userId: user.id }); - - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveFileEntityService: DriveFileEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: me.id }); - const files = await query.take(ps.limit).getMany(); + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); + const files = await query.take(ps.limit).getMany(); + + return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 07064ce9fa..8a497a514e 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -1,5 +1,6 @@ -import define from '../../define.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmailService } from '@/core/EmailService.js'; export const meta = { tags: ['users'], @@ -31,6 +32,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await validateEmailForAccount(ps.emailAddress); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private emailService: EmailService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.emailService.validateEmailForAccount(ps.emailAddress); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index c174126779..2141dfbeb0 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,4 +1,5 @@ -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; export const meta = { @@ -16,13 +17,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const ep = endpoints.find(x => x.name === ps.endpoint); - if (ep == null) return null; - return { - params: Object.entries(ep.params.properties || {}).map(([k, v]) => ({ - name: k, - type: v.type.charAt(0).toUpperCase() + v.type.slice(1), - })), - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async (ps) => { + const ep = endpoints.find(x => x.name === ps.endpoint); + if (ep == null) return null; + return { + params: Object.entries(ep.params.properties || {}).map(([k, v]) => ({ + name: k, + type: v.type.charAt(0).toUpperCase() + v.type.slice(1), + })), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index b20da96eb3..91fc3ec98d 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,4 +1,5 @@ -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; export const meta = { @@ -29,6 +30,12 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - return endpoints.map(x => x.name); -}); +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async () => { + return endpoints.map(x => x.name); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index 5fe622932d..ead6b037cc 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,6 +1,7 @@ import ms from 'ms'; -import { createExportCustomEmojisJob } from '@/queue/index.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportCustomEmojisJob(user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportCustomEmojisJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index 7b1197d1e5..e5222fcbfd 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Followings } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FollowingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -30,13 +33,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followeeHost = :host`, { host: ps.host }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - const followings = await query - .take(ps.limit) - .getMany(); + private followingEntityService: FollowingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere('following.followeeHost = :host', { host: ps.host }); - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); + const followings = await query + .take(ps.limit) + .getMany(); + + return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index ed1f142d88..a20c5a31b3 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Followings } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FollowingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -30,13 +33,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followerHost = :host`, { host: ps.host }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - const followings = await query - .take(ps.limit) - .getMany(); + private followingEntityService: FollowingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere('following.followerHost = :host', { host: ps.host }); - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); + const followings = await query + .take(ps.limit) + .getMany(); + + return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 07e5c07c6a..e7f8cefff5 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,7 +1,9 @@ -import config from '@/config/index.js'; -import define from '../../define.js'; -import { Instances } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InstancesRepository } from '@/models/index.js'; +import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -37,82 +39,93 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Instances.createQueryBuilder('instance'); - - switch (ps.sort) { - case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break; - case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break; - case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; - case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; - case '+users': query.orderBy('instance.usersCount', 'DESC'); break; - case '-users': query.orderBy('instance.usersCount', 'ASC'); break; - case '+following': query.orderBy('instance.followingCount', 'DESC'); break; - case '-following': query.orderBy('instance.followingCount', 'ASC'); break; - case '+followers': query.orderBy('instance.followersCount', 'DESC'); break; - case '-followers': query.orderBy('instance.followersCount', 'ASC'); break; - case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; - case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; - case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; - case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; - - default: query.orderBy('instance.id', 'DESC'); break; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private instanceEntityService: InstanceEntityService, + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.instancesRepository.createQueryBuilder('instance'); + + switch (ps.sort) { + case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break; + case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break; + case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; + case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; + case '+users': query.orderBy('instance.usersCount', 'DESC'); break; + case '-users': query.orderBy('instance.usersCount', 'ASC'); break; + case '+following': query.orderBy('instance.followingCount', 'DESC'); break; + case '-following': query.orderBy('instance.followingCount', 'ASC'); break; + case '+followers': query.orderBy('instance.followersCount', 'DESC'); break; + case '-followers': query.orderBy('instance.followersCount', 'ASC'); break; + case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; + case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; + case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; + case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; + + default: query.orderBy('instance.id', 'DESC'); break; + } + + if (typeof ps.blocked === 'boolean') { + const meta = await this.metaService.fetch(true); + if (ps.blocked) { + query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); + } else { + query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); + } + } + + if (typeof ps.notResponding === 'boolean') { + if (ps.notResponding) { + query.andWhere('instance.isNotResponding = TRUE'); + } else { + query.andWhere('instance.isNotResponding = FALSE'); + } + } + + if (typeof ps.suspended === 'boolean') { + if (ps.suspended) { + query.andWhere('instance.isSuspended = TRUE'); + } else { + query.andWhere('instance.isSuspended = FALSE'); + } + } + + if (typeof ps.federating === 'boolean') { + if (ps.federating) { + query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); + } else { + query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))'); + } + } + + if (typeof ps.subscribing === 'boolean') { + if (ps.subscribing) { + query.andWhere('instance.followersCount > 0'); + } else { + query.andWhere('instance.followersCount = 0'); + } + } + + if (typeof ps.publishing === 'boolean') { + if (ps.publishing) { + query.andWhere('instance.followingCount > 0'); + } else { + query.andWhere('instance.followingCount = 0'); + } + } + + if (ps.host) { + query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); + } + + const instances = await query.take(ps.limit).skip(ps.offset).getMany(); + + return await this.instanceEntityService.packMany(instances); + }); } - - if (typeof ps.blocked === 'boolean') { - const meta = await fetchMeta(true); - if (ps.blocked) { - query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); - } else { - query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); - } - } - - if (typeof ps.notResponding === 'boolean') { - if (ps.notResponding) { - query.andWhere('instance.isNotResponding = TRUE'); - } else { - query.andWhere('instance.isNotResponding = FALSE'); - } - } - - if (typeof ps.suspended === 'boolean') { - if (ps.suspended) { - query.andWhere('instance.isSuspended = TRUE'); - } else { - query.andWhere('instance.isSuspended = FALSE'); - } - } - - if (typeof ps.federating === 'boolean') { - if (ps.federating) { - query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); - } else { - query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))'); - } - } - - if (typeof ps.subscribing === 'boolean') { - if (ps.subscribing) { - query.andWhere('instance.followersCount > 0'); - } else { - query.andWhere('instance.followersCount = 0'); - } - } - - if (typeof ps.publishing === 'boolean') { - if (ps.publishing) { - query.andWhere('instance.followingCount > 0'); - } else { - query.andWhere('instance.followingCount = 0'); - } - } - - if (ps.host) { - query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); - } - - const instances = await query.take(ps.limit).skip(ps.offset).getMany(); - - return await Instances.packMany(instances); -}); +} diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 2fbb8a15cb..f855b54537 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InstancesRepository } from '@/models/index.js'; +import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -26,9 +29,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances - .findOneBy({ host: toPuny(ps.host) }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, - return instance ? await Instances.pack(instance) : null; -}); + private utilityService: UtilityService, + private instanceEntityService: InstanceEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.instancesRepository + .findOneBy({ host: this.utilityService.toPuny(ps.host) }); + + return instance ? await this.instanceEntityService.pack(instance) : null; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index e02c7b97e0..d07a08637f 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -1,7 +1,10 @@ import { IsNull, MoreThan, Not } from 'typeorm'; -import { Followings, Instances } from '@/models/index.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -21,45 +24,58 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([ - Instances.find({ - where: { - followersCount: MoreThan(0), - }, - order: { - followersCount: 'DESC', - }, - take: ps.limit, - }), - Instances.find({ - where: { - followingCount: MoreThan(0), - }, - order: { - followingCount: 'DESC', - }, - take: ps.limit, - }), - Followings.count({ - where: { - followeeHost: Not(IsNull()), - }, - }), - Followings.count({ - where: { - followerHost: Not(IsNull()), - }, - }), - ]); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, - const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0); - const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - return await awaitAll({ - topSubInstances: Instances.packMany(topSubInstances), - otherFollowersCount: Math.max(0, allSubCount - gotSubCount), - topPubInstances: Instances.packMany(topPubInstances), - otherFollowingCount: Math.max(0, allPubCount - gotPubCount), - }); -}); + private instanceEntityService: InstanceEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([ + this.instancesRepository.find({ + where: { + followersCount: MoreThan(0), + }, + order: { + followersCount: 'DESC', + }, + take: ps.limit, + }), + this.instancesRepository.find({ + where: { + followingCount: MoreThan(0), + }, + order: { + followingCount: 'DESC', + }, + take: ps.limit, + }), + this.followingsRepository.count({ + where: { + followeeHost: Not(IsNull()), + }, + }), + this.followingsRepository.count({ + where: { + followerHost: Not(IsNull()), + }, + }), + ]); + + const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0); + const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); + + return await awaitAll({ + topSubInstances: this.instanceEntityService.packMany(topSubInstances), + otherFollowersCount: Math.max(0, allSubCount - gotSubCount), + topPubInstances: this.instanceEntityService.packMany(topPubInstances), + otherFollowingCount: Math.max(0, allPubCount - gotPubCount), + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index 409cc7695e..57497bbf62 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { getRemoteUser } from '../../common/getters.js'; -import { updatePerson } from '@/remote/activitypub/models/person.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ApPersonService } from '@/core/remote/activitypub/models/ApPersonService.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['federation'], @@ -17,7 +18,15 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await getRemoteUser(ps.userId); - await updatePerson(user.uri!); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private getterService: GetterService, + private apPersonService: ApPersonService, + ) { + super(meta, paramDef, async (ps) => { + const user = await this.getterService.getRemoteUser(ps.userId); + await this.apPersonService.updatePerson(user.uri!); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 65ad9f88d3..0400cacd02 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -30,13 +33,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId) - .andWhere(`user.host = :host`, { host: ps.host }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const users = await query - .take(ps.limit) - .getMany(); + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.usersRepository.createQueryBuilder('user'), ps.sinceId, ps.untilId) + .andWhere('user.host = :host', { host: ps.host }); - return await Users.packMany(users, me, { detail: true }); -}); + const users = await query + .take(ps.limit) + .getMany(); + + return await this.userEntityService.packMany(users, me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 05fa22a9e4..9e6a3cc717 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -1,7 +1,9 @@ import Parser from 'rss-parser'; -import { getResponse } from '@/misc/fetch.js'; -import config from '@/config/index.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; const rssParser = new Parser(); @@ -22,18 +24,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const res = await getResponse({ - url: ps.url, - method: 'GET', - headers: Object.assign({ - 'User-Agent': config.userAgent, - Accept: 'application/rss+xml, */*', - }), - timeout: 5000, - }); - - const text = await res.text(); - - return rssParser.parseString(text); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + super(meta, paramDef, async (ps, me) => { + const res = await this.httpRequestService.getResponse({ + url: ps.url, + method: 'GET', + headers: Object.assign({ + 'User-Agent': config.userAgent, + Accept: 'application/rss+xml, */*', + }), + timeout: 5000, + }); + + const text = await res.text(); + + return rssParser.parseString(text); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 02a030cd5e..3a06c63d52 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,10 +1,13 @@ import ms from 'ms'; -import create from '@/services/following/create.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Followings, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['following', 'users'], @@ -66,39 +69,54 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const follower = user; - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFollowing); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = me; + + // 自分自身 + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followeeIsYourself); + } + + // Get followee + const followee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check if already following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyFollowing); + } + + try { + await this.userFollowingService.follow(follower, followee); + } catch (e) { + if (e instanceof IdentifiableError) { + if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); + if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); + } + throw e; + } + + return await this.userEntityService.pack(followee.id, me); + }); } - - try { - await create(follower, followee); - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); - if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); - } - throw e; - } - - return await Users.pack(followee.id, user); -}); +} diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 2f41b16e9a..07366bc821 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import deleteFollowing from '@/services/following/delete.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Followings, Users } from '@/models/index.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['following', 'users'], @@ -53,31 +56,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const follower = user; - - // Check if the followee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = me; + + // Check if the followee is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followeeIsYourself); + } + + // Get followee + const followee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFollowing); + } + + await this.userFollowingService.unfollow(follower, followee); + + return await this.userEntityService.pack(followee.id, me); + }); } - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await deleteFollowing(follower, followee); - - return await Users.pack(followee.id, user); -}); +} diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 18ec5affe8..8285189d66 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import deleteFollowing from '@/services/following/delete.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Followings, Users } from '@/models/index.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['following', 'users'], @@ -53,31 +56,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const followee = user; - - // Check if the follower is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followerIsYourself); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const followee = me; + + // Check if the follower is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followerIsYourself); + } + + // Get follower + const follower = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFollowing); + } + + await this.userFollowingService.unfollow(follower, followee); + + return await this.userEntityService.pack(followee.id, me); + }); } - - // Get follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await deleteFollowing(follower, followee); - - return await Users.pack(followee.id, user); -}); +} diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index e5df55375e..5f082c087a 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,7 +1,8 @@ -import acceptFollowRequest from '@/services/following/requests/accept.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['following', 'account'], @@ -33,17 +34,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await acceptFollowRequest(user, follower).catch(e => { - if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest); - throw e; - }); - - return; -}); +@Injectable() +export default class extends Endpoint { + constructor( + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch follower + const follower = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + await this.userFollowingService.acceptFollowRequest(me, follower).catch(err => { + if (err.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest); + throw err; + }); + + return; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 80d37fb075..0b79a80649 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,9 +1,13 @@ -import cancelFollowRequest from '@/services/following/requests/cancel.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; -import { Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['following', 'account'], @@ -42,21 +46,33 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - try { - await cancelFollowRequest(followee, user); - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound); - } - throw e; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - return await Users.pack(followee.id, user); -}); + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch followee + const followee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + try { + await this.userFollowingService.cancelFollowRequest(followee, me); + } catch (err) { + if (err instanceof IdentifiableError) { + if (err.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound); + } + throw err; + } + + return await this.userEntityService.pack(followee.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index a8f42c481d..c36d4a077f 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,5 +1,8 @@ -import define from '../../../define.js'; -import { FollowRequests } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FollowRequestsRepository } from '@/models/index.js'; +import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['following', 'account'], @@ -42,10 +45,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const reqs = await FollowRequests.findBy({ - followeeId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, - return await Promise.all(reqs.map(req => FollowRequests.pack(req))); -}); + private followRequestEntityService: FollowRequestEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const reqs = await this.followRequestsRepository.findBy({ + followeeId: me.id, + }); + + return await Promise.all(reqs.map(req => this.followRequestEntityService.pack(req))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index cebe604284..663c659bf3 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,7 +1,8 @@ -import { rejectFollowRequest } from '@/services/following/reject.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['following', 'account'], @@ -28,14 +29,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await rejectFollowRequest(user, follower); - - return; -}); +@Injectable() +export default class extends Endpoint { + constructor( + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch follower + const follower = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + await this.userFollowingService.rejectFollowRequest(me, follower); + + return; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index e6acd36911..3b892ef522 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { GalleryPosts } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['gallery'], @@ -24,13 +27,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const posts = await query.take(10).getMany(); + private galleryPostEntityService: GalleryPostEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.galleryPostsRepository.createQueryBuilder('post') + .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) + .andWhere('post.likedCount > 0') + .orderBy('post.likedCount', 'DESC'); - return await GalleryPosts.packMany(posts, me); -}); + const posts = await query.take(10).getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index c4c8982fcc..551ea64835 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { GalleryPosts } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['gallery'], @@ -24,12 +27,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const posts = await query.take(10).getMany(); + private galleryPostEntityService: GalleryPostEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.galleryPostsRepository.createQueryBuilder('post') + .andWhere('post.likedCount > 0') + .orderBy('post.likedCount', 'DESC'); - return await GalleryPosts.packMany(posts, me); -}); + const posts = await query.take(10).getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index 428ba9cc71..4afcbce816 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { GalleryPosts } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['gallery'], @@ -27,11 +30,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('post.user', 'user'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const posts = await query.take(ps.limit).getMany(); + private galleryPostEntityService: GalleryPostEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + .innerJoinAndSelect('post.user', 'user'); - return await GalleryPosts.packMany(posts, me); -}); + const posts = await query.take(ps.limit).getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 8074a3b34f..9e8bcac131 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,10 +1,13 @@ import ms from 'ms'; -import define from '../../../define.js'; -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { genId } from '../../../../../misc/gen-id.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { IdService } from '@/core/IdService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; export const meta = { tags: ['gallery'], @@ -43,28 +46,42 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOneBy({ - id: fileId, - userId: user.id, - }) - ))).filter((file): file is DriveFile => file != null); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - if (files.length === 0) { - throw new Error(); - } + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private galleryPostEntityService: GalleryPostEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter((file): file is DriveFile => file != null); - const post = await GalleryPosts.insert(new GalleryPost({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - description: ps.description, - userId: user.id, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), - })).then(x => GalleryPosts.findOneByOrFail(x.identifiers[0])); + if (files.length === 0) { + throw new Error(); + } - return await GalleryPosts.pack(post, user); -}); + const post = await this.galleryPostsRepository.insert(new GalleryPost({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: new Date(), + title: ps.title, + description: ps.description, + userId: me.id, + isSensitive: ps.isSensitive, + fileIds: files.map(file => file.id), + })).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.galleryPostEntityService.pack(post, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index b00ee0e2ae..ad5f95c853 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { GalleryPosts } from '@/models/index.js'; export const meta = { tags: ['gallery'], @@ -27,15 +29,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - userId: user.id, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const post = await this.galleryPostsRepository.findOneBy({ + id: ps.postId, + userId: me.id, + }); + + if (post == null) { + throw new ApiError(meta.errors.noSuchPost); + } + + await this.galleryPostsRepository.delete(post.id); + }); } - - await GalleryPosts.delete(post.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index b858114aec..8aca98119b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { GalleryPosts, GalleryLikes } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['gallery'], @@ -40,33 +42,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - if (post.userId === user.id) { - throw new ApiError(meta.errors.yourPost); - } + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, - // if already liked - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); + if (post == null) { + throw new ApiError(meta.errors.noSuchPost); + } - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } + if (post.userId === me.id) { + throw new ApiError(meta.errors.yourPost); + } - // Create like - await GalleryLikes.insert({ - id: genId(), - createdAt: new Date(), - postId: post.id, - userId: user.id, - }); + // if already liked + const exist = await this.galleryLikesRepository.findOneBy({ + postId: post.id, + userId: me.id, + }); - GalleryPosts.increment({ id: post.id }, 'likedCount', 1); -}); + if (exist != null) { + throw new ApiError(meta.errors.alreadyLiked); + } + + // Create like + await this.galleryLikesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + postId: post.id, + userId: me.id, + }); + + this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index 4f6dafd7cb..723906d60f 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { GalleryPosts } from '@/models/index.js'; export const meta = { tags: ['gallery'], @@ -31,14 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, + + private galleryPostEntityService: GalleryPostEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const post = await this.galleryPostsRepository.findOneBy({ + id: ps.postId, + }); + + if (post == null) { + throw new ApiError(meta.errors.noSuchPost); + } + + return await this.galleryPostEntityService.pack(post, me); + }); } - - return await GalleryPosts.pack(post, me); -}); +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index d136239e5e..d878582998 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository, GalleryLikesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { GalleryPosts, GalleryLikes } from '@/models/index.js'; export const meta = { tags: ['gallery'], @@ -33,23 +35,34 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); + if (post == null) { + throw new ApiError(meta.errors.noSuchPost); + } - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } + const exist = await this.galleryLikesRepository.findOneBy({ + postId: post.id, + userId: me.id, + }); - // Delete like - await GalleryLikes.delete(exist.id); + if (exist == null) { + throw new ApiError(meta.errors.notLiked); + } - GalleryPosts.decrement({ id: post.id }, 'likedCount', 1); -}); + // Delete like + await this.galleryLikesRepository.delete(exist.id); + + this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 82fe38078e..1900afaeb6 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,9 +1,12 @@ import ms from 'ms'; -import define from '../../../define.js'; -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; export const meta = { tags: ['gallery'], @@ -43,30 +46,43 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOneBy({ - id: fileId, - userId: user.id, - }) - ))).filter((file): file is DriveFile => file != null); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - if (files.length === 0) { - throw new Error(); - } + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private galleryPostEntityService: GalleryPostEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter((file): file is DriveFile => file != null); - await GalleryPosts.update({ - id: ps.postId, - userId: user.id, - }, { - updatedAt: new Date(), - title: ps.title, - description: ps.description, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), - }); + if (files.length === 0) { + throw new Error(); + } - const post = await GalleryPosts.findOneByOrFail({ id: ps.postId }); + await this.galleryPostsRepository.update({ + id: ps.postId, + userId: me.id, + }, { + updatedAt: new Date(), + title: ps.title, + description: ps.description, + isSensitive: ps.isSensitive, + fileIds: files.map(file => file.id), + }); - return await GalleryPosts.pack(post, user); -}); + const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); + + return await this.galleryPostEntityService.pack(post, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index 56c5502978..2d9bf29dd9 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,7 +1,9 @@ import { MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; -import { Users } from '@/models/index.js'; -import define from '../define.js'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['meta'], @@ -16,12 +18,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const count = await Users.countBy({ - lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)), - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async () => { + const count = await this.usersRepository.countBy({ + lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)), + }); - return { - count, - }; -}); + return { + count, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 50e36386cf..a7e7e6ba35 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { Hashtags } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { HashtagsRepository } from '@/models/index.js'; +import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['hashtags'], @@ -30,39 +33,49 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Hashtags.createQueryBuilder('tag'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, - if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); - if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0'); - if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0'); + private hashtagEntityService: HashtagEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.hashtagsRepository.createQueryBuilder('tag'); - switch (ps.sort) { - case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break; - case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break; - case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break; - case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break; - case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break; - case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break; - case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break; - case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break; - case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break; - case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break; - case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break; - case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break; - } + if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); + if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0'); + if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0'); + + switch (ps.sort) { + case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break; + case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break; + case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break; + case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break; + case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break; + case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break; + case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break; + case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break; + case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break; + case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break; + case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break; + case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break; + } - query.select([ - 'tag.name', - 'tag.mentionedUsersCount', - 'tag.mentionedLocalUsersCount', - 'tag.mentionedRemoteUsersCount', - 'tag.attachedUsersCount', - 'tag.attachedLocalUsersCount', - 'tag.attachedRemoteUsersCount', - ]); + query.select([ + 'tag.name', + 'tag.mentionedUsersCount', + 'tag.mentionedLocalUsersCount', + 'tag.mentionedRemoteUsersCount', + 'tag.attachedUsersCount', + 'tag.attachedLocalUsersCount', + 'tag.attachedRemoteUsersCount', + ]); - const tags = await query.take(ps.limit).getMany(); + const tags = await query.take(ps.limit).getMany(); - return Hashtags.packMany(tags); -}); + return this.hashtagEntityService.packMany(tags); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index c289844775..3fb77bef9b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { Hashtags } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { HashtagsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['hashtags'], @@ -27,14 +29,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const hashtags = await Hashtags.createQueryBuilder('tag') - .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) - .orderBy('tag.count', 'DESC') - .groupBy('tag.id') - .take(ps.limit) - .skip(ps.offset) - .getMany(); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') + .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) + .orderBy('tag.count', 'DESC') + .groupBy('tag.id') + .take(ps.limit) + .skip(ps.offset) + .getMany(); - return hashtags.map(tag => tag.name); -}); + return hashtags.map(tag => tag.name); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 5b78f6ac7f..59170f6d0e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -1,7 +1,10 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { Hashtags } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { HashtagsRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['hashtags'], @@ -32,11 +35,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) }); - if (hashtag == null) { - throw new ApiError(meta.errors.noSuchHashtag); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, - return await Hashtags.pack(hashtag); -}); + private hashtagEntityService: HashtagEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const hashtag = await this.hashtagsRepository.findOneBy({ name: normalizeForSearch(ps.tag) }); + if (hashtag == null) { + throw new ApiError(meta.errors.noSuchHashtag); + } + + return await this.hashtagEntityService.pack(hashtag); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 9cdbc8941c..7e483ea214 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,10 +1,12 @@ import { Brackets } from 'typeorm'; -import define from '../../define.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotesRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 @@ -60,94 +62,104 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const instance = await fetchMeta(true); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - - const now = new Date(); // 5分単位で丸めた現在日時 - now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); - - const tagNotes = await Notes.createQueryBuilder('note') - .where(`note.createdAt > :date`, { date: new Date(now.getTime() - rangeA) }) - .andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })) - .andWhere(`note.tags != '{}'`) - .select(['note.tags', 'note.userId']) - .cache(60000) // 1 min - .getMany(); - - if (tagNotes.length === 0) { - return []; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private metaService: MetaService, + ) { + super(meta, paramDef, async () => { + const instance = await this.metaService.fetch(true); + const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + + const now = new Date(); // 5分単位で丸めた現在日時 + now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); + + const tagNotes = await this.notesRepository.createQueryBuilder('note') + .where('note.createdAt > :date', { date: new Date(now.getTime() - rangeA) }) + .andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.tags != \'{}\'') + .select(['note.tags', 'note.userId']) + .cache(60000) // 1 min + .getMany(); + + if (tagNotes.length === 0) { + return []; + } - const tags: { + const tags: { name: string; users: Note['userId'][]; }[] = []; - for (const note of tagNotes) { - for (const tag of note.tags) { - if (hiddenTags.includes(tag)) continue; - - const x = tags.find(x => x.name === tag); - if (x) { - if (!x.users.includes(note.userId)) { - x.users.push(note.userId); + for (const note of tagNotes) { + for (const tag of note.tags) { + if (hiddenTags.includes(tag)) continue; + + const x = tags.find(x => x.name === tag); + if (x) { + if (!x.users.includes(note.userId)) { + x.users.push(note.userId); + } + } else { + tags.push({ + name: tag, + users: [note.userId], + }); + } } - } else { - tags.push({ - name: tag, - users: [note.userId], - }); } - } - } - // タグを人気順に並べ替え - const hots = tags - .sort((a, b) => b.users.length - a.users.length) - .map(tag => tag.name) - .slice(0, max); - - //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する - const countPromises: Promise[] = []; - - const range = 20; - - // 10分 - const interval = 1000 * 60 * 10; - - for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) - .cache(60000) // 1 min - .getRawOne() - .then(x => parseInt(x.count, 10)) - ))); - } + // タグを人気順に並べ替え + const hots = tags + .sort((a, b) => b.users.length - a.users.length) + .map(tag => tag.name) + .slice(0, max); + + //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する + const countPromises: Promise[] = []; + + const range = 20; + + // 10分 + const interval = 1000 * 60 * 10; + + for (let i = 0; i < range; i++) { + countPromises.push(Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) + .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) + .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) + .cache(60000) // 1 min + .getRawOne() + .then(x => parseInt(x.count, 10)), + ))); + } - const countsLog = await Promise.all(countPromises); - //#endregion - - const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) - .cache(60000 * 60) // 60 min - .getRawOne() - .then(x => parseInt(x.count, 10)) - )); - - const stats = hots.map((tag, i) => ({ - tag, - chart: countsLog.map(counts => counts[i]), - usersCount: totalCounts[i], - })); - - return stats; -}); + const countsLog = await Promise.all(countPromises); + //#endregion + + const totalCounts = await Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) + .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) + .cache(60000 * 60) // 60 min + .getRawOne() + .then(x => parseInt(x.count, 10)), + )); + + const stats = hots.map((tag, i) => ({ + tag, + chart: countsLog.map(counts => counts[i]), + usersCount: totalCounts[i], + })); + + return stats; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index a5df21a7e3..10a88fbefa 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: false, @@ -24,39 +27,49 @@ export const paramDef = { tag: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'alive'], default: "all" }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, + state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, }, required: ['tag', 'sort'], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user') - .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.usersRepository.createQueryBuilder('user') + .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }); - const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); + const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); - if (ps.state === 'alive') { - query.andWhere('user.updatedAt > :date', { date: recent }); - } + if (ps.state === 'alive') { + query.andWhere('user.updatedAt > :date', { date: recent }); + } - if (ps.origin === 'local') { - query.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - query.andWhere('user.host IS NOT NULL'); - } + if (ps.origin === 'local') { + query.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('user.host IS NOT NULL'); + } - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; - case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; - } + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + } - const users = await query.take(ps.limit).getMany(); + const users = await query.take(ps.limit).getMany(); - return await Users.packMany(users, me, { detail: true }); -}); + return await this.userEntityService.packMany(users, me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 22aedfeee8..815b3168b4 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,8 @@ -import { Users } from '@/models/index.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -20,12 +23,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, token) => { - const isSecure = token == null; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す - return await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); -}); + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, user, token) => { + const isSecure = token == null; + + // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す + return await this.userEntityService.pack(user.id, user, { + detail: true, + includeSecrets: isSecure, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 35806b2bc3..bcf3931b04 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,6 +1,8 @@ import * as speakeasy from 'speakeasy'; -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,27 +19,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const token = ps.token.replace(/\s/g, ''); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.twoFactorTempSecret == null) { - throw new Error('二段階認証の設定が開始されていません'); - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: 'base32', - token: token, - }); - - if (!verified) { - throw new Error('not verified'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const token = ps.token.replace(/\s/g, ''); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + if (profile.twoFactorTempSecret == null) { + throw new Error('二段階認証の設定が開始されていません'); + } + + const verified = (speakeasy as any).totp.verify({ + secret: profile.twoFactorTempSecret, + encoding: 'base32', + token: token, + }); + + if (!verified) { + throw new Error('not verified'); + } + + await this.userProfilesRepository.update(me.id, { + twoFactorSecret: profile.twoFactorTempSecret, + twoFactorEnabled: true, + }); + }); } - - await UserProfiles.update(user.id, { - twoFactorSecret: profile.twoFactorTempSecret, - twoFactorEnabled: true, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 1afb34bfda..f2f4c2044e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -1,19 +1,16 @@ -import bcrypt from 'bcryptjs'; import { promisify } from 'node:util'; +import bcrypt from 'bcryptjs'; import * as cbor from 'cbor'; -import define from '../../../define.js'; -import { - UserProfiles, - UserSecurityKeys, - AttestationChallenges, - Users, -} from '@/models/index.js'; -import config from '@/config/index.js'; -import { procedures, hash } from '../../../2fa.js'; -import { publishMainStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; const cborDecodeFirst = promisify(cbor.decodeFirst) as any; -const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8')); export const meta = { requireCredential: true, @@ -34,110 +31,135 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new Error('incorrect password'); - } - - if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, - const clientData = JSON.parse(ps.clientDataJSON); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (clientData.type !== 'webauthn.create') { - throw new Error('not a creation attestation'); - } - if (clientData.origin !== config.scheme + '://' + config.host) { - throw new Error('origin mismatch'); - } - - const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8')); - - const attestation = await cborDecodeFirst(ps.attestationObject); - - const rpIdHash = attestation.authData.slice(0, 32); - if (!rpIdHashReal.equals(rpIdHash)) { - throw new Error('rpIdHash mismatch'); - } + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, - const flags = attestation.authData[32]; + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, - // eslint:disable-next-line:no-bitwise - if (!(flags & 1)) { - throw new Error('user not present'); - } - - const authData = Buffer.from(attestation.authData); - const credentialIdLength = authData.readUInt16BE(53); - const credentialId = authData.slice(55, 55 + credentialIdLength); - const publicKeyData = authData.slice(55 + credentialIdLength); - const publicKey: Map = await cborDecodeFirst(publicKeyData); - if (publicKey.get(3) !== -7) { - throw new Error('alg mismatch'); - } - - if (!(procedures as any)[attestation.fmt]) { - throw new Error('unsupported fmt'); - } - - const verificationData = (procedures as any)[attestation.fmt].verify({ - attStmt: attestation.attStmt, - authenticatorData: authData, - clientDataHash: clientDataJSONHash, - credentialId, - publicKey, - rpIdHash, - }); - if (!verificationData.valid) throw new Error('signature invalid'); - - const attestationChallenge = await AttestationChallenges.findOneBy({ - userId: user.id, - id: ps.challengeId, - registrationChallenge: true, - challenge: hash(clientData.challenge).toString('hex'), - }); - - if (!attestationChallenge) { - throw new Error('non-existent challenge'); - } - - await AttestationChallenges.delete({ - userId: user.id, - id: ps.challengeId, - }); - - // Expired challenge (> 5min old) - if ( - new Date().getTime() - attestationChallenge.createdAt.getTime() >= - 5 * 60 * 1000 + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, ) { - throw new Error('expired challenge'); + super(meta, paramDef, async (ps, me) => { + const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8')); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } + + if (!profile.twoFactorEnabled) { + throw new Error('2fa not enabled'); + } + + const clientData = JSON.parse(ps.clientDataJSON); + + if (clientData.type !== 'webauthn.create') { + throw new Error('not a creation attestation'); + } + if (clientData.origin !== this.config.scheme + '://' + this.config.host) { + throw new Error('origin mismatch'); + } + + const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8')); + + const attestation = await cborDecodeFirst(ps.attestationObject); + + const rpIdHash = attestation.authData.slice(0, 32); + if (!rpIdHashReal.equals(rpIdHash)) { + throw new Error('rpIdHash mismatch'); + } + + const flags = attestation.authData[32]; + + // eslint:disable-next-line:no-bitwise + if (!(flags & 1)) { + throw new Error('user not present'); + } + + const authData = Buffer.from(attestation.authData); + const credentialIdLength = authData.readUInt16BE(53); + const credentialId = authData.slice(55, 55 + credentialIdLength); + const publicKeyData = authData.slice(55 + credentialIdLength); + const publicKey: Map = await cborDecodeFirst(publicKeyData); + if (publicKey.get(3) !== -7) { + throw new Error('alg mismatch'); + } + + const procedures = this.twoFactorAuthenticationService.getProcedures(); + + if (!(procedures as any)[attestation.fmt]) { + throw new Error('unsupported fmt'); + } + + const verificationData = (procedures as any)[attestation.fmt].verify({ + attStmt: attestation.attStmt, + authenticatorData: authData, + clientDataHash: clientDataJSONHash, + credentialId, + publicKey, + rpIdHash, + }); + if (!verificationData.valid) throw new Error('signature invalid'); + + const attestationChallenge = await this.attestationChallengesRepository.findOneBy({ + userId: me.id, + id: ps.challengeId, + registrationChallenge: true, + challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), + }); + + if (!attestationChallenge) { + throw new Error('non-existent challenge'); + } + + await this.attestationChallengesRepository.delete({ + userId: me.id, + id: ps.challengeId, + }); + + // Expired challenge (> 5min old) + if ( + new Date().getTime() - attestationChallenge.createdAt.getTime() >= + 5 * 60 * 1000 + ) { + throw new Error('expired challenge'); + } + + const credentialIdString = credentialId.toString('hex'); + + await this.userSecurityKeysRepository.insert({ + userId: me.id, + id: credentialIdString, + lastUsed: new Date(), + name: ps.name, + publicKey: verificationData.publicKey.toString('hex'), + }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); + + return { + id: credentialIdString, + name: ps.name, + }; + }); } - - const credentialIdString = credentialId.toString('hex'); - - await UserSecurityKeys.insert({ - userId: user.id, - id: credentialIdString, - lastUsed: new Date(), - name: ps.name, - publicKey: verificationData.publicKey.toString('hex'), - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - })); - - return { - id: credentialIdString, - name: ps.name, - }; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index 4bfa24f97f..3eb9f43c2b 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -16,8 +18,16 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await UserProfiles.update(user.id, { - usePasswordLessLogin: ps.value, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.userProfilesRepository.update(me.id, { + usePasswordLessLogin: ps.value, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index e906b82043..df37db4c6a 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -1,10 +1,12 @@ -import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles, AttestationChallenges } from '@/models/index.js'; import { promisify } from 'node:util'; import * as crypto from 'node:crypto'; -import { genId } from '@/misc/gen-id.js'; -import { hash } from '../../../2fa.js'; +import bcrypt from 'bcryptjs'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { DI } from '@/di-symbols.js'; const randomBytes = promisify(crypto.randomBytes); @@ -23,39 +25,53 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, - if (!same) { - throw new Error('incorrect password'); - } + private idService: IdService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } - // 32 byte challenge - const entropy = await randomBytes(32); - const challenge = entropy.toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); - - const challengeId = genId(); - - await AttestationChallenges.insert({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: true, - }); - - return { - challengeId, - challenge, - }; -}); + if (!profile.twoFactorEnabled) { + throw new Error('2fa not enabled'); + } + + // 32 byte challenge + const entropy = await randomBytes(32); + const challenge = entropy.toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const challengeId = this.idService.genId(); + + await this.attestationChallengesRepository.insert({ + userId: me.id, + id: challengeId, + challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), + createdAt: new Date(), + registrationChallenge: true, + }); + + return { + challengeId, + challenge, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 33f5717728..e20911f35e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,9 +1,11 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; -import config from '@/config/index.js'; -import { UserProfiles } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; export const meta = { requireCredential: true, @@ -20,39 +22,50 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (!same) { - throw new Error('incorrect password'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } + + // Generate user's secret key + const secret = speakeasy.generateSecret({ + length: 32, + }); - // Generate user's secret key - const secret = speakeasy.generateSecret({ - length: 32, - }); - - await UserProfiles.update(user.id, { - twoFactorTempSecret: secret.base32, - }); - - // Get the data URL of the authenticator URL - const url = speakeasy.otpauthURL({ - secret: secret.base32, - encoding: 'base32', - label: user.username, - issuer: config.host, - }); - const dataUrl = await QRCode.toDataURL(url); - - return { - qr: dataUrl, - url, - secret: secret.base32, - label: user.username, - issuer: config.host, - }; -}); + await this.userProfilesRepository.update(me.id, { + twoFactorTempSecret: secret.base32, + }); + + // Get the data URL of the authenticator URL + const url = speakeasy.otpauthURL({ + secret: secret.base32, + encoding: 'base32', + label: me.username, + issuer: this.config.host, + }); + const dataUrl = await QRCode.toDataURL(url); + + return { + qr: dataUrl, + url, + secret: secret.base32, + label: me.username, + issuer: this.config.host, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index eb2f75308d..1889dd7893 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -1,7 +1,11 @@ import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,27 +23,41 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new Error('incorrect password'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } + + // Make sure we only delete the user's own creds + await this.userSecurityKeysRepository.delete({ + userId: me.id, + id: ps.credentialId, + }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); + + return {}; + }); } - - // Make sure we only delete the user's own creds - await UserSecurityKeys.delete({ - userId: user.id, - id: ps.credentialId, - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - })); - - return {}; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 45e7a98639..4607e5d981 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,6 +1,8 @@ import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,18 +19,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - if (!same) { - throw new Error('incorrect password'); - } + if (!same) { + throw new Error('incorrect password'); + } - await UserProfiles.update(user.id, { - twoFactorSecret: null, - twoFactorEnabled: false, - }); -}); + await this.userProfilesRepository.update(me.id, { + twoFactorSecret: null, + twoFactorEnabled: false, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index eca9558847..8d5851659b 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { AccessTokens } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -16,25 +18,33 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = AccessTokens.createQueryBuilder('token') - .where('token.userId = :userId', { userId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.accessTokensRepository.createQueryBuilder('token') + .where('token.userId = :userId', { userId: me.id }); - switch (ps.sort) { - case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; - case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break; - case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break; - default: query.orderBy('token.id', 'ASC'); break; - } + switch (ps.sort) { + case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; + case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break; + case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break; + default: query.orderBy('token.id', 'ASC'); break; + } - const tokens = await query.getMany(); + const tokens = await query.getMany(); - return await Promise.all(tokens.map(token => ({ - id: token.id, - name: token.name, - createdAt: token.createdAt, - lastUsedAt: token.lastUsedAt, - permission: token.permission, - }))); -}); + return await Promise.all(tokens.map(token => ({ + id: token.id, + name: token.name, + createdAt: token.createdAt, + lastUsedAt: token.lastUsedAt, + permission: token.permission, + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 68bd103a6d..a5592d20de 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { AccessTokens, Apps } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -12,26 +15,36 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - sort: { type: 'string', enum: ['desc', 'asc'], default: "desc" }, + sort: { type: 'string', enum: ['desc', 'asc'], default: 'desc' }, }, required: [], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get tokens - const tokens = await AccessTokens.find({ - where: { - userId: user.id, - }, - take: ps.limit, - skip: ps.offset, - order: { - id: ps.sort === 'asc' ? 1 : -1, - }, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, - return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, { - detail: true, - }))); -}); + private appEntityService: AppEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get tokens + const tokens = await this.accessTokensRepository.find({ + where: { + userId: me.id, + }, + take: ps.limit, + skip: ps.offset, + order: { + id: ps.sort === 'asc' ? 1 : -1, + }, + }); + + return await Promise.all(tokens.map(token => this.appEntityService.pack(token.appId, me, { + detail: true, + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index f9f6a33a80..cc5b712ecf 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,6 +1,8 @@ import bcrypt from 'bcryptjs'; -import define from '../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,21 +20,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await bcrypt.compare(ps.currentPassword, profile.password!); - - if (!same) { - throw new Error('incorrect password'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.currentPassword, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.newPassword, salt); + + await this.userProfilesRepository.update(me.id, { + password: hash, + }); + }); } - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(ps.newPassword, salt); - - await UserProfiles.update(user.id, { - password: hash, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index ede4a9d03b..a1804599df 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,7 +1,9 @@ import bcrypt from 'bcryptjs'; -import { UserProfiles, Users } from '@/models/index.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DeleteAccountService } from '@/core/DeleteAccountService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const userDetailed = await Users.findOneByOrFail({ id: user.id }); - if (userDetailed.isDeleted) { - return; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (!same) { - throw new Error('incorrect password'); - } + private deleteAccountService: DeleteAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); + if (userDetailed.isDeleted) { + return; + } + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - await deleteAccount(user); -}); + if (!same) { + throw new Error('incorrect password'); + } + + await this.deleteAccountService.deleteAccount(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index aed4c2e0a3..770708e685 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportBlockingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportBlockingJob(user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportBlockingJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index 058d77b3c2..fcaa59b12d 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportFollowingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -21,6 +22,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportFollowingJob(user, ps.excludeMuting, ps.excludeInactive); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportFollowingJob(me, ps.excludeMuting, ps.excludeInactive); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index c0216fac0c..37bef0a117 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportMuteJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportMuteJob(user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportMuteJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index 4b85a45554..9d2505e403 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportNotesJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportNotesJob(user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportNotesJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index fa5c1f5e5a..0f8e4bca76 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportUserListsJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportUserListsJob(user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportUserListsJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3c420e4d0f..350abd9f7b 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { NoteFavorites } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteFavoritesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteFavoriteEntityService } from '@/core/entities/NoteFavoriteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'notes', 'favorites'], @@ -31,14 +34,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) - .andWhere(`favorite.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('favorite.note', 'note'); - - const favorites = await query - .take(ps.limit) - .getMany(); - - return await NoteFavorites.packMany(favorites, user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + private noteFavoriteEntityService: NoteFavoriteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) + .andWhere('favorite.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('favorite.note', 'note'); + + const favorites = await query + .take(ps.limit) + .getMany(); + + return await this.noteFavoriteEntityService.packMany(favorites, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index a38383f30e..ff6bcc01ab 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { GalleryLikes } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryLikesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryLikeEntityService } from '@/core/entities/GalleryLikeEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'gallery'], @@ -27,7 +30,7 @@ export const meta = { ref: 'GalleryPost', }, }, - } + }, }, } as const; @@ -42,14 +45,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.post', 'post'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, - const likes = await query - .take(ps.limit) - .getMany(); + private galleryLikeEntityService: GalleryLikeEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + .andWhere('like.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('like.post', 'post'); - return await GalleryLikes.packMany(likes, user); -}); + const likes = await query + .take(ps.limit) + .getMany(); + + return await this.galleryLikeEntityService.packMany(likes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index b4edb5f73d..927be51f79 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { GalleryPosts } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'gallery'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere(`post.userId = :meId`, { meId: user.id }); - - const posts = await query - .take(ps.limit) - .getMany(); - - return await GalleryPosts.packMany(posts, user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, + + private galleryPostEntityService: GalleryPostEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + .andWhere('post.userId = :meId', { meId: me.id }); + + const posts = await query + .take(ps.limit) + .getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts index e7d7518c5b..0695abdd85 100644 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { MutedNotes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MutedNotesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -27,11 +29,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - return { - count: await MutedNotes.countBy({ - userId: user.id, - reason: 'word', - }), - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + return { + count: await this.mutedNotesRepository.countBy({ + userId: me.id, + reason: 'word', + }), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 0bcbf37ddd..bfba1fc36f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportBlockingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -49,13 +51,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportBlockingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportBlockingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index ee2abbea19..c7cb2e0330 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportFollowingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -48,13 +50,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportFollowingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportFollowingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index b3b3b39238..060c37c13f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportMutingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -49,13 +51,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportMutingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportMutingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 64f5ec05fd..a5e17283e5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportUserListsJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -48,13 +50,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportUserListsJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportUserListsJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 2b343dabdd..96927dad49 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,10 +1,13 @@ import { Brackets } from 'typeorm'; -import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; import { notificationTypes } from '@/types.js'; -import read from '@/services/note/read.js'; -import { readNotification } from '../../common/read-notification.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'notifications'], @@ -49,96 +52,121 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // includeTypes が空の場合はクエリしない - if (ps.includeTypes && ps.includeTypes.length === 0) { - return []; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, + + private notificationEntityService: NotificationEntityService, + private notificationService: NotificationService, + private queryService: QueryService, + private noteReadService: NoteReadService, + ) { + super(meta, paramDef, async (ps, me) => { + // includeTypes が空の場合はクエリしない + if (ps.includeTypes && ps.includeTypes.length === 0) { + return []; + } + // excludeTypes に全指定されている場合はクエリしない + if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { + return []; + } + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: me.id }); + + const suspendedQuery = this.usersRepository.createQueryBuilder('users') + .select('users.id') + .where('users.isSuspended = TRUE'); + + const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId) + .andWhere('notification.notifieeId = :meId', { meId: me.id }) + .leftJoinAndSelect('notification.notifier', 'notifier') + .leftJoinAndSelect('notification.note', 'note') + .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') + .leftJoinAndSelect('notifier.banner', 'notifierBanner') + .leftJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + // muted users + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); + query.setParameters(mutingQuery.getParameters()); + + // muted instances + query.andWhere(new Brackets(qb => { qb + .andWhere('notifier.host IS NULL') + .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); + })); + query.setParameters(mutingInstanceQuery.getParameters()); + + // suspended users + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); + + if (ps.following) { + query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id }); + query.setParameters(followingQuery.getParameters()); + } + + if (ps.includeTypes && ps.includeTypes.length > 0) { + query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes }); + } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { + query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes }); + } + + if (ps.unreadOnly) { + query.andWhere('notification.isRead = false'); + } + + const notifications = await query.take(ps.limit).getMany(); + + // Mark all as read + if (notifications.length > 0 && ps.markAsRead) { + this.notificationService.readNotification(me.id, notifications.map(x => x.id)); + } + + const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); + + if (notes.length > 0) { + this.noteReadService.read(me.id, notes); + } + + return await this.notificationEntityService.packMany(notifications, me.id); + }); } - // excludeTypes に全指定されている場合はクエリしない - if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { - return []; - } - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: user.id }); - - const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile') - .select('user_profile.mutedInstances') - .where('user_profile.userId = :muterId', { muterId: user.id }); - - const suspendedQuery = Users.createQueryBuilder('users') - .select('users.id') - .where('users.isSuspended = TRUE'); - - const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) - .andWhere('notification.notifieeId = :meId', { meId: user.id }) - .leftJoinAndSelect('notification.notifier', 'notifier') - .leftJoinAndSelect('notification.note', 'note') - .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') - .leftJoinAndSelect('notifier.banner', 'notifierBanner') - .leftJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - - // muted users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - query.setParameters(mutingQuery.getParameters()); - - // muted instances - query.andWhere(new Brackets(qb => { qb - .andWhere('notifier.host IS NULL') - .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); - })); - query.setParameters(mutingInstanceQuery.getParameters()); - - // suspended users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - - if (ps.following) { - query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); - query.setParameters(followingQuery.getParameters()); - } - - if (ps.includeTypes && ps.includeTypes.length > 0) { - query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes }); - } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { - query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes }); - } - - if (ps.unreadOnly) { - query.andWhere('notification.isRead = false'); - } - - const notifications = await query.take(ps.limit).getMany(); - - // Mark all as read - if (notifications.length > 0 && ps.markAsRead) { - readNotification(user.id, notifications.map(x => x.id)); - } - - const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); - - if (notes.length > 0) { - read(user.id, notes); - } - - return await Notifications.packMany(notifications, user.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index 71e326e2f0..9a909eedf4 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { PageLikes } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageLikesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { PageLikeEntityService } from '@/core/entities/PageLikeEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'pages'], @@ -26,7 +29,7 @@ export const meta = { ref: 'Page', }, }, - } + }, }, } as const; @@ -41,14 +44,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.page', 'page'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, - const likes = await query - .take(ps.limit) - .getMany(); + private pageLikeEntityService: PageLikeEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.pageLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + .andWhere('like.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('like.page', 'page'); - return PageLikes.packMany(likes, user); -}); + const likes = await query + .take(ps.limit) + .getMany(); + + return this.pageLikeEntityService.packMany(likes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index f28aed3fd4..7c4e4a6c7d 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Pages } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PagesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'pages'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :meId`, { meId: user.id }); - - const pages = await query - .take(ps.limit) - .getMany(); - - return await Pages.packMany(pages); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + private pageEntityService: PageEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId) + .andWhere('page.userId = :meId', { meId: me.id }); + + const pages = await query + .take(ps.limit) + .getMany(); + + return await this.pageEntityService.packMany(pages); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index 67b7026be1..f31b0dc35e 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,7 +1,9 @@ -import { addPinned } from '@/services/i/pin.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['account', 'notes'], @@ -46,15 +48,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await addPinned(user, ps.noteId).catch(e => { - if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote); - if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded); - if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned); - throw e; - }); - - return await Users.pack(user.id, user, { - detail: true, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private userEntityService: UserEntityService, + private notePiningService: NotePiningService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.notePiningService.addPinned(me, ps.noteId).catch(err => { + if (err.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded); + if (err.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned); + throw err; + }); + + return await this.userEntityService.pack(me.id, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts index 7ff6409caf..36c3566f55 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,6 +1,8 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; -import { MessagingMessages, UserGroupJoinings } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'messaging'], @@ -17,25 +19,38 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Update documents - await MessagingMessages.update({ - recipientId: user.id, - isRead: false, - }, { - isRead: true, - }); - - const joinings = await UserGroupJoinings.findBy({ userId: user.id }); - - await Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${user.id}')`) as any, - }) - .where(`groupId = :groupId`, { groupId: j.userGroupId }) - .andWhere('userId != :userId', { userId: user.id }) - .andWhere('NOT (:userId = ANY(reads))', { userId: user.id }) - .execute())); - - publishMainStream(user.id, 'readAllMessagingMessages'); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Update documents + await this.messagingMessagesRepository.update({ + recipientId: me.id, + isRead: false, + }, { + isRead: true, + }); + + const joinings = await this.userGroupJoiningsRepository.findBy({ userId: me.id }); + + await Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder().update() + .set({ + reads: (() => `array_append("reads", '${me.id}')`) as any, + }) + .where('groupId = :groupId', { groupId: j.userGroupId }) + .andWhere('userId != :userId', { userId: me.id }) + .andWhere('NOT (:userId = ANY(reads))', { userId: me.id }) + .execute())); + + this.globalEventService.publishMainStream(me.id, 'readAllMessagingMessages'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index 49f3deb331..b4bb83c6eb 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,6 +1,8 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; -import { NoteUnreads } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteUnreadsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -17,13 +19,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Remove documents - await NoteUnreads.delete({ - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, - // 全て既読になったイベントを発行 - publishMainStream(user.id, 'readAllUnreadMentions'); - publishMainStream(user.id, 'readAllUnreadSpecifiedNotes'); -}); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Remove documents + await this.noteUnreadsRepository.delete({ + userId: me.id, + }); + + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions'); + this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 45b6e98c86..5a7909674f 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,8 +1,12 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { genId } from '@/misc/gen-id.js'; -import { AnnouncementReads, Announcements, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; export const meta = { tags: ['account'], @@ -29,33 +33,48 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Check if announcement exists - const announcement = await Announcements.findOneBy({ id: ps.announcementId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, - // Check if already read - const read = await AnnouncementReads.findOneBy({ - announcementId: ps.announcementId, - userId: user.id, - }); + private userEntityService: UserEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Check if announcement exists + const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); - if (read != null) { - return; - } + if (announcement == null) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const read = await this.announcementReadsRepository.findOneBy({ + announcementId: ps.announcementId, + userId: me.id, + }); + + if (read != null) { + return; + } - // Create read - await AnnouncementReads.insert({ - id: genId(), - createdAt: new Date(), - announcementId: ps.announcementId, - userId: user.id, - }); + // Create read + await this.announcementReadsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + announcementId: ps.announcementId, + userId: me.id, + }); - if (!await Users.getHasUnreadAnnouncement(user.id)) { - publishMainStream(user.id, 'readAllAnnouncements'); + if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) { + this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements'); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index af929b04e8..7796fd97cb 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,8 +1,10 @@ import bcrypt from 'bcryptjs'; -import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; -import generateUserToken from '../../common/generate-native-user-token.js'; -import define from '../../define.js'; -import { Users, UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import generateUserToken from '@/misc/generate-native-user-token.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,31 +21,44 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const freshUser = await Users.findOneByOrFail({ id: user.id }); - const oldToken = freshUser.token; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id }); + const oldToken = freshUser.token; - if (!same) { - throw new Error('incorrect password'); - } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - const newToken = generateUserToken(); + if (!same) { + throw new Error('incorrect password'); + } - await Users.update(user.id, { - token: newToken, - }); + const newToken = generateUserToken(); - // Publish event - publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken }); - publishMainStream(user.id, 'myTokenRegenerated'); + await this.usersRepository.update(me.id, { + token: newToken, + }); - // Terminate streaming - setTimeout(() => { - publishUserEvent(user.id, 'terminate', {}); - }, 5000); -}); + // Publish event + this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken }); + this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated'); + + // Terminate streaming + setTimeout(() => { + this.globalEventService.publishUserEvent(me.id, 'terminate', {}); + }, 5000); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index d0b16dbc48..3b4db5fae3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); - - const res = {} as Record; - - for (const item of items) { - res[item.key] = item.value; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const items = await query.getMany(); + + const res = {} as Record; + + for (const item of items) { + res[item.key] = item.value; + } + + return res; + }); } - - return res; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index cc5d5a8c6f..d24dff95b0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,21 +30,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); + + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + return { + updatedAt: item.updatedAt, + value: item.value, + }; + }); } - - return { - updatedAt: item.updatedAt, - value: item.value, - }; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index a79319744c..98d94a4c02 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,18 +30,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); + + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + return item.value; + }); } - - return item.value; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index ac209c06a6..d1a05d9d06 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); - - const res = {} as Record; - - for (const item of items) { - const type = typeof item.value; - res[item.key] = +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const items = await query.getMany(); + + const res = {} as Record; + + for (const item of items) { + const type = typeof item.value; + res[item.key] = item.value === null ? 'null' : Array.isArray(item.value) ? 'array' : type === 'number' ? 'number' : @@ -38,7 +46,9 @@ export default define(meta, paramDef, async (ps, user) => { type === 'boolean' ? 'boolean' : type === 'object' ? 'object' : null as never; - } + } - return res; -}); + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 5ea1a9d344..6df5f4ecc3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,14 +20,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .select('item.key') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.key') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const items = await query.getMany(); + const items = await query.getMany(); - return items.map(x => x.key); -}); + return items.map(x => x.key); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index 92473654c6..b5870f099d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,18 +30,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); + + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + await this.registryItemsRepository.remove(item); + }); } - - await RegistryItems.remove(item); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index de4b313e25..58085ddbc5 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -14,20 +16,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .select('item.scope') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }); - - const items = await query.getMany(); - - const res = [] as string[][]; - - for (const item of items) { - if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; - res.push(item.scope); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.scope') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }); + + const items = await query.getMany(); + + const res = [] as string[][]; + + for (const item of items) { + if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; + res.push(item.scope); + } + + return res; + }); } - - return res; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index d380b428a3..585aac2e01 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,7 +1,9 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -22,37 +24,48 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await RegistryItems.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await RegistryItems.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - userId: user.id, - domain: null, - scope: ps.scope, - key: ps.key, - value: ps.value, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const existingItem = await query.getOne(); + + if (existingItem) { + await this.registryItemsRepository.update(existingItem.id, { + updatedAt: new Date(), + value: ps.value, + }); + } else { + await this.registryItemsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: new Date(), + userId: me.id, + domain: null, + scope: ps.scope, + key: ps.key, + value: ps.value, + }); + } + + // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする + this.globalEventService.publishMainStream(me.id, 'registryUpdated', { + scope: ps.scope, + key: ps.key, + value: ps.value, + }); }); } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - publishMainStream(user.id, 'registryUpdated', { - scope: ps.scope, - key: ps.key, - value: ps.value, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c692453794..86a82e6a6c 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,6 +1,8 @@ -import define from '../../define.js'; -import { AccessTokens } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,16 +19,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const token = await AccessTokens.findOneBy({ id: ps.tokenId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, - if (token) { - await AccessTokens.delete({ - id: ps.tokenId, - userId: user.id, - }); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId }); + + if (token) { + await this.accessTokensRepository.delete({ + id: ps.tokenId, + userId: me.id, + }); - // Terminate streaming - publishUserEvent(user.id, 'terminate'); + // Terminate streaming + this.globalEventService.publishUserEvent(me.id, 'terminate'); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index ca37411662..410cd72065 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Signins } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SigninsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,11 +22,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId) - .andWhere(`signin.userId = :meId`, { meId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, - const history = await query.take(ps.limit).getMany(); + private signinEntityService: SigninEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId) + .andWhere('signin.userId = :meId', { meId: me.id }); - return await Promise.all(history.map(record => Signins.pack(record))); -}); + const history = await query.take(ps.limit).getMany(); + + return await Promise.all(history.map(record => this.signinEntityService.pack(record))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 9912689da5..9a735e1168 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,7 +1,9 @@ -import { removePinned } from '@/services/i/pin.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['account', 'notes'], @@ -34,13 +36,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await removePinned(user, ps.noteId).catch(e => { - if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - return await Users.pack(user.id, user, { - detail: true, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private userEntityService: UserEntityService, + private notePiningService: NotePiningService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.notePiningService.removePinned(me, ps.noteId).catch(err => { + if (err.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + return await this.userEntityService.pack(me.id, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 3318078523..719cc14f09 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,13 +1,15 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; -import config from '@/config/index.js'; import ms from 'ms'; import bcrypt from 'bcryptjs'; -import { Users, UserProfiles } from '@/models/index.js'; -import { sendEmail } from '@/services/send-email.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; export const meta = { requireCredential: true, @@ -44,50 +46,68 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new ApiError(meta.errors.incorrectPassword); - } - - if (ps.email != null) { - const available = await validateEmailForAccount(ps.email); - if (!available) { - throw new ApiError(meta.errors.unavailable); - } - } - - await UserProfiles.update(user.id, { - email: ps.email, - emailVerified: false, - emailVerifyCode: null, - }); - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - - if (ps.email != null) { - const code = rndstr('a-z0-9', 16); - - await UserProfiles.update(user.id, { - emailVerifyCode: code, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private emailService: EmailService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new ApiError(meta.errors.incorrectPassword); + } + + if (ps.email != null) { + const available = await this.emailService.validateEmailForAccount(ps.email); + if (!available) { + throw new ApiError(meta.errors.unavailable); + } + } + + await this.userProfilesRepository.update(me.id, { + email: ps.email, + emailVerified: false, + emailVerifyCode: null, + }); + + const iObj = await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj); + + if (ps.email != null) { + const code = rndstr('a-z0-9', 16); + + await this.userProfilesRepository.update(me.id, { + emailVerifyCode: code, + }); + + const link = `${this.config.url}/verify-email/${code}`; + + this.emailService.sendEmail(ps.email, 'Email verification', + `To verify email, please click this link:
${link}`, + `To verify email, please click this link: ${link}`); + } + + return iObj; }); - - const link = `${config.url}/verify-email/${code}`; - - sendEmail(ps.email, 'Email verification', - `To verify email, please click this link:
${link}`, - `To verify email, please click this link: ${link}`); } - - return iObj; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 3c2f1cea0d..4b904d4696 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,19 +1,23 @@ import RE2 from 're2'; import * as mfm from 'mfm-js'; -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import acceptAllFollowRequests from '@/services/following/requests/accept-all.js'; -import { publishToFollowers } from '@/services/i/update.js'; +import { Inject, Injectable } from '@nestjs/common'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import { updateUsertags } from '@/services/update-hashtag.js'; -import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; +import { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { HashtagService } from '@/core/HashtagService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import define from '../../define.js'; export const meta = { tags: ['account'], @@ -70,10 +74,10 @@ export const meta = { export const paramDef = { type: 'object', properties: { - name: { ...Users.nameSchema, nullable: true }, - description: { ...Users.descriptionSchema, nullable: true }, - location: { ...Users.locationSchema, nullable: true }, - birthday: { ...Users.birthdaySchema, nullable: true }, + name: { ...nameSchema, nullable: true }, + description: { ...descriptionSchema, nullable: true }, + location: { ...locationSchema, nullable: true }, + birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -122,134 +126,157 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, _user, token) => { - const user = await Users.findOneByOrFail({ id: _user.id }); - const isSecure = token == null; - - const updates = {} as Partial; - const profileUpdates = {} as Partial; - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (ps.name !== undefined) updates.name = ps.name; - if (ps.description !== undefined) profileUpdates.description = ps.description; - if (ps.lang !== undefined) profileUpdates.lang = ps.lang; - if (ps.location !== undefined) profileUpdates.location = ps.location; - if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; - if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; - if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; - if (ps.mutedWords !== undefined) { - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); - if (!regexp) throw new ApiError(meta.errors.invalidRegexp); - - try { - new RE2(regexp[1], regexp[2]); - } catch (err) { - throw new ApiError(meta.errors.invalidRegexp); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private userFollowingService: UserFollowingService, + private accountUpdateService: AccountUpdateService, + private hashtagService: HashtagService, + ) { + super(meta, paramDef, async (ps, _user, token) => { + const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); + const isSecure = token == null; + + const updates = {} as Partial; + const profileUpdates = {} as Partial; + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + if (ps.name !== undefined) updates.name = ps.name; + if (ps.description !== undefined) profileUpdates.description = ps.description; + if (ps.lang !== undefined) profileUpdates.lang = ps.lang; + if (ps.location !== undefined) profileUpdates.location = ps.location; + if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; + if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; + if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; + if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; + if (ps.mutedWords !== undefined) { + // validate regular expression syntax + ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { + const regexp = x.match(/^\/(.+)\/(.*)$/); + if (!regexp) throw new ApiError(meta.errors.invalidRegexp); + + try { + new RE2(regexp[1], regexp[2]); + } catch (err) { + throw new ApiError(meta.errors.invalidRegexp); + } + }); + + profileUpdates.mutedWords = ps.mutedWords; + profileUpdates.enableWordMute = ps.mutedWords.length > 0; + } + if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; + if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; + if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; + if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; + if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; + if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; + if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; + if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; + if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; + if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; + if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; + if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; + if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; + if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; + if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; + if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; + if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; + + if (ps.avatarId) { + const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); + + if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); + if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); } - }); - - profileUpdates.mutedWords = ps.mutedWords; - profileUpdates.enableWordMute = ps.mutedWords.length > 0; - } - if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; - if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; - if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; - if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; - if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; - if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; - if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; - if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; - if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; - if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; - if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; - if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; - if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; - if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; - if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; - if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; - if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - - if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); - - if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); - if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); - } - if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); + if (ps.bannerId) { + const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); - if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); - if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); - } + if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); + if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); + } - if (ps.pinnedPageId) { - const page = await Pages.findOneBy({ id: ps.pinnedPageId }); + if (ps.pinnedPageId) { + const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId }); - if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); + if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); - profileUpdates.pinnedPageId = page.id; - } else if (ps.pinnedPageId === null) { - profileUpdates.pinnedPageId = null; - } + profileUpdates.pinnedPageId = page.id; + } else if (ps.pinnedPageId === null) { + profileUpdates.pinnedPageId = null; + } - if (ps.fields) { - profileUpdates.fields = ps.fields - .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') - .map(x => { - return { name: x.name, value: x.value }; - }); - } + if (ps.fields) { + profileUpdates.fields = ps.fields + .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') + .map(x => { + return { name: x.name, value: x.value }; + }); + } - //#region emojis/tags + //#region emojis/tags - let emojis = [] as string[]; - let tags = [] as string[]; + let emojis = [] as string[]; + let tags = [] as string[]; - const newName = updates.name === undefined ? user.name : updates.name; - const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; + const newName = updates.name === undefined ? user.name : updates.name; + const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; - if (newName != null) { - const tokens = mfm.parseSimple(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - } + if (newName != null) { + const tokens = mfm.parseSimple(newName); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + } - if (newDescription != null) { - const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); - } + if (newDescription != null) { + const tokens = mfm.parse(newDescription); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); + } - updates.emojis = emojis; - updates.tags = tags; + updates.emojis = emojis; + updates.tags = tags; - // ハッシュタグ更新 - updateUsertags(user, tags); - //#endregion + // ハッシュタグ更新 + this.hashtagService.updateUsertags(user, tags); + //#endregion - if (Object.keys(updates).length > 0) await Users.update(user.id, updates); - if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); + if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); + if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates); - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); + const iObj = await this.userEntityService.pack(user.id, user, { + detail: true, + includeSecrets: isSecure, + }); - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOneBy({ userId: user.id })); + // Publish meUpdated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); + this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneBy({ userId: user.id })); - // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && ps.isLocked === false) { - acceptAllFollowRequests(user); - } + // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 + if (user.isLocked && ps.isLocked === false) { + this.userFollowingService.acceptAllFollowRequests(user); + } - // フォロワーにUpdateを配信 - publishToFollowers(user.id); + // フォロワーにUpdateを配信 + this.accountUpdateService.publishToFollowers(user.id); - return iObj; -}); + return iObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts index 1d7e4a16b3..6dd1626bb8 100644 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { UserGroupInvitations } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupInvitationsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserGroupInvitationEntityService } from '@/core/entities/UserGroupInvitationEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'groups'], @@ -42,14 +45,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) - .andWhere(`invitation.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('invitation.userGroup', 'user_group'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, - const invitations = await query - .take(ps.limit) - .getMany(); + private userGroupInvitationEntityService: UserGroupInvitationEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.userGroupInvitationsRepository.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) + .andWhere('invitation.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('invitation.userGroup', 'user_group'); - return await UserGroupInvitations.packMany(invitations); -}); + const invitations = await query + .take(ps.limit) + .getMany(); + + return await this.userGroupInvitationEntityService.packMany(invitations); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 2e2fd00b8c..016b1b5d6a 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; -import { genId } from '@/misc/gen-id.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { webhookEventTypes } from '@/models/entities/webhook.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['webhooks'], @@ -25,19 +27,32 @@ export const paramDef = { required: ['name', 'url', 'secret', 'on'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - }).then(x => Webhooks.findOneByOrFail(x.identifiers[0])); - - publishInternalEvent('webhookCreated', webhook); - - return webhook; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('webhookCreated', webhook); + + return webhook; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 2821eaa5f1..53b553b43e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['webhooks'], @@ -27,18 +29,30 @@ export const paramDef = { required: ['webhookId'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); + + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } + + await this.webhooksRepository.delete(webhook.id); + + this.globalEventService.publishInternalEvent('webhookDeleted', webhook); + }); } - - await Webhooks.delete(webhook.id); - - publishInternalEvent('webhookDeleted', webhook); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 54e4563732..8e4aff45dd 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Webhooks } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['webhooks', 'account'], @@ -16,10 +18,18 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const webhooks = await Webhooks.findBy({ - userId: me.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const webhooks = await this.webhooksRepository.findBy({ + userId: me.id, + }); - return webhooks; -}); + return webhooks; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 02fa1edb5e..622c2ade98 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; export const meta = { tags: ['webhooks'], @@ -27,15 +29,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); + + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } + + return webhook; + }); } - - return webhook; -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index f87b9753fb..3a0ef1a526 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { webhookEventTypes } from '@/models/entities/webhook.js'; export const meta = { tags: ['webhooks'], @@ -36,24 +38,36 @@ export const paramDef = { required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); - await Webhooks.update(webhook.id, { - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - active: ps.active, - }); + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } - publishInternalEvent('webhookUpdated', webhook); -}); + await this.webhooksRepository.update(webhook.id, { + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + active: ps.active, + }); + + this.globalEventService.publishInternalEvent('webhookUpdated', webhook); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts index ea0600d0e4..da3ba59df9 100644 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ b/packages/backend/src/server/api/endpoints/messaging/history.ts @@ -1,7 +1,10 @@ -import define from '../../define.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { MessagingMessages, Mutings, UserGroupJoinings } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js'; +import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['messaging'], @@ -31,61 +34,77 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const mute = await Mutings.findBy({ - muterId: user.id, - }); - - const groups = ps.group ? await UserGroupJoinings.findBy({ - userId: user.id, - }).then(xs => xs.map(x => x.userGroupId)) : []; - - if (ps.group && groups.length === 0) { - return []; - } - - const history: MessagingMessage[] = []; - - for (let i = 0; i < ps.limit; i++) { - const found = ps.group - ? history.map(m => m.groupId!) - : history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!); - - const query = MessagingMessages.createQueryBuilder('message') - .orderBy('message.createdAt', 'DESC'); - - if (ps.group) { - query.where(`message.groupId IN (:...groups)`, { groups: groups }); - - if (found.length > 0) { - query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); - } - } else { - query.where(new Brackets(qb => { qb - .where(`message.userId = :userId`, { userId: user.id }) - .orWhere(`message.recipientId = :userId`, { userId: user.id }); - })); - query.andWhere(`message.groupId IS NULL`); - - if (found.length > 0) { - query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); - query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private messagingMessageEntityService: MessagingMessageEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const mute = await this.mutingsRepository.findBy({ + muterId: me.id, + }); + + const groups = ps.group ? await this.userGroupJoiningsRepository.findBy({ + userId: me.id, + }).then(xs => xs.map(x => x.userGroupId)) : []; + + if (ps.group && groups.length === 0) { + return []; } - if (mute.length > 0) { - query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); - query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + const history: MessagingMessage[] = []; + + for (let i = 0; i < ps.limit; i++) { + const found = ps.group + ? history.map(m => m.groupId!) + : history.map(m => (m.userId === me.id) ? m.recipientId! : m.userId!); + + const query = this.messagingMessagesRepository.createQueryBuilder('message') + .orderBy('message.createdAt', 'DESC'); + + if (ps.group) { + query.where('message.groupId IN (:...groups)', { groups: groups }); + + if (found.length > 0) { + query.andWhere('message.groupId NOT IN (:...found)', { found: found }); + } + } else { + query.where(new Brackets(qb => { qb + .where('message.userId = :userId', { userId: me.id }) + .orWhere('message.recipientId = :userId', { userId: me.id }); + })); + query.andWhere('message.groupId IS NULL'); + + if (found.length > 0) { + query.andWhere('message.userId NOT IN (:...found)', { found: found }); + query.andWhere('message.recipientId NOT IN (:...found)', { found: found }); + } + + if (mute.length > 0) { + query.andWhere('message.userId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) }); + query.andWhere('message.recipientId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) }); + } + } + + const message = await query.getOne(); + + if (message) { + history.push(message); + } else { + break; + } } - } - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } + return await Promise.all(history.map(h => this.messagingMessageEntityService.pack(h.id, me))); + }); } - - return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user))); -}); +} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index dbf1f6c868..6579b03987 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -1,10 +1,15 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { MessagingMessages, UserGroups, UserGroupJoinings, Users } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; +import { MessagingService } from '@/core/MessagingService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['messaging'], @@ -69,73 +74,93 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - if (ps.userId != null) { - // Fetch recipient (user) - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(new Brackets(qb => { qb - .where('message.userId = :meId') - .andWhere('message.recipientId = :recipientId'); - })) - .orWhere(new Brackets(qb => { qb - .where('message.userId = :recipientId') - .andWhere('message.recipientId = :meId'); - })); - })) - .setParameter('meId', user.id) - .setParameter('recipientId', recipient.id); - - const messages = await query.take(ps.limit).getMany(); - - // Mark all as read - if (ps.markAsRead) { - readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id)); - - // リモートユーザーとのメッセージだったら既読配信 - if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) { - deliverReadActivity(user, recipient, messages); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + @Inject(DI.userGroupsRepository) + private userGroupRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private messagingMessageEntityService: MessagingMessageEntityService, + private messagingService: MessagingService, + private userEntityService: UserEntityService, + private queryService: QueryService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.userId != null) { + // Fetch recipient (user) + const recipient = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(new Brackets(qb => { qb + .where('message.userId = :meId') + .andWhere('message.recipientId = :recipientId'); + })) + .orWhere(new Brackets(qb => { qb + .where('message.userId = :recipientId') + .andWhere('message.recipientId = :meId'); + })); + })) + .setParameter('meId', me.id) + .setParameter('recipientId', recipient.id); + + const messages = await query.take(ps.limit).getMany(); + + // Mark all as read + if (ps.markAsRead) { + this.messagingService.readUserMessagingMessage(me.id, recipient.id, messages.filter(m => m.recipientId === me.id).map(x => x.id)); + + // リモートユーザーとのメッセージだったら既読配信 + if (this.userEntityService.isLocalUser(me) && this.userEntityService.isRemoteUser(recipient)) { + this.messagingService.deliverReadActivity(me, recipient, messages); + } + } + + return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { + populateRecipient: false, + }))); + } else if (ps.groupId != null) { + // Fetch recipient (group) + const recipientGroup = await this.userGroupRepository.findOneBy({ id: ps.groupId }); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userId: me.id, + userGroupId: recipientGroup.id, + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + + const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere('message.groupId = :groupId', { groupId: recipientGroup.id }); + + const messages = await query.take(ps.limit).getMany(); + + // Mark all as read + if (ps.markAsRead) { + this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id)); + } + + return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { + populateGroup: false, + }))); } - } - - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateRecipient: false, - }))); - } else if (ps.groupId != null) { - // Fetch recipient (group) - const recipientGroup = await UserGroups.findOneBy({ id: ps.groupId }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOneBy({ - userId: user.id, - userGroupId: recipientGroup.id, }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } - - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id }); - - const messages = await query.take(ps.limit).getMany(); - - // Mark all as read - if (ps.markAsRead) { - readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id)); - } - - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateGroup: false, - }))); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index 405af5ec17..e02afcbcfd 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -1,10 +1,12 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { MessagingService } from '@/core/MessagingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; -import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { createMessage } from '@/services/messages/create.js'; export const meta = { tags: ['messaging'], @@ -87,65 +89,85 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let recipientUser: User | null; - let recipientGroup: UserGroup | null; - - if (ps.userId != null) { - // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } - - // Fetch recipient (user) - recipientUser = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private getterService: GetterService, + private messagingService: MessagingService, + ) { + super(meta, paramDef, async (ps, me) => { + let recipientUser: User | null; + let recipientGroup: UserGroup | null; + + if (ps.userId != null) { + // Myself + if (ps.userId === me.id) { + throw new ApiError(meta.errors.recipientIsYourself); + } + + // Fetch recipient (user) + recipientUser = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check blocking + const block = await this.blockingsRepository.findOneBy({ + blockerId: recipientUser.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } else if (ps.groupId != null) { + // Fetch recipient (group) + recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! }); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userId: me.id, + userGroupId: recipientGroup.id, + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + } + + let file = null; + if (ps.fileId != null) { + file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + // テキストが無いかつ添付ファイルも無かったらエラー + if (ps.text == null && file == null) { + throw new ApiError(meta.errors.contentRequired); + } + + return await this.messagingService.createMessage(me, recipientUser, recipientGroup, ps.text, file); }); - - // Check blocking - const block = await Blockings.findOneBy({ - blockerId: recipientUser.id, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } else if (ps.groupId != null) { - // Fetch recipient (group) - recipientGroup = await UserGroups.findOneBy({ id: ps.groupId! }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOneBy({ - userId: user.id, - userGroupId: recipientGroup.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } } - - let file = null; - if (ps.fileId != null) { - file = await DriveFiles.findOneBy({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - return await createMessage(user, recipientUser, recipientGroup, ps.text, file); -}); +} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts index f66d75873c..5baecb9114 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MessagingMessagesRepository } from '@/models/index.js'; +import { MessagingService } from '@/core/MessagingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { MessagingMessages } from '@/models/index.js'; -import { deleteMessage } from '@/services/messages/delete.js'; export const meta = { tags: ['messaging'], @@ -35,15 +37,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const message = await MessagingMessages.findOneBy({ - id: ps.messageId, - userId: user.id, - }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + private messagingService: MessagingService, + ) { + super(meta, paramDef, async (ps, me) => { + const message = await this.messagingMessagesRepository.findOneBy({ + id: ps.messageId, + userId: me.id, + }); + + if (message == null) { + throw new ApiError(meta.errors.noSuchMessage); + } + + await this.messagingService.deleteMessage(message); + }); } - - await deleteMessage(message); -}); +} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts index db12ae922c..6e66cafe1e 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MessagingMessagesRepository } from '@/models/index.js'; +import { MessagingService } from '@/core/MessagingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { MessagingMessages } from '@/models/index.js'; -import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message.js'; export const meta = { tags: ['messaging'], @@ -28,22 +30,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const message = await MessagingMessages.findOneBy({ id: ps.messageId }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - if (message.recipientId) { - await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => { - if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); - throw e; - }); - } else if (message.groupId) { - await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { - if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); - throw e; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + private messagingService: MessagingService, + ) { + super(meta, paramDef, async (ps, me) => { + const message = await this.messagingMessagesRepository.findOneBy({ id: ps.messageId }); + + if (message == null) { + throw new ApiError(meta.errors.noSuchMessage); + } + + if (message.recipientId) { + await this.messagingService.readUserMessagingMessage(me.id, message.userId, [message.id]).catch(err => { + if (err.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); + throw err; + }); + } else if (message.groupId) { + await this.messagingService.readGroupMessagingMessage(me.id, message.groupId, [message.id]).catch(err => { + if (err.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); + throw err; + }); + } }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5b624842c3..9a6258d7dd 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,10 +1,14 @@ import { IsNull, MoreThan } from 'typeorm'; -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Ads, Emojis, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { AdsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import define from '../define.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['meta'], @@ -26,7 +30,6 @@ export const meta = { version: { type: 'string', optional: false, nullable: false, - example: config.version, }, name: { type: 'string', @@ -304,111 +307,132 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const emojis = await Emojis.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - cache: { - id: 'meta_emojis', - milliseconds: 3600000, // 1 hour - }, - }); + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, - const ads = await Ads.find({ - where: { - expiresAt: MoreThan(new Date()), - }, - }); + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, - const response: any = { - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, + private userEntityService: UserEntityService, + private emojiEntityService: EmojiEntityService, + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.metaService.fetch(true); - version: config.version, + const emojis = await this.emojisRepository.find({ + where: { + host: IsNull(), + }, + order: { + category: 'ASC', + name: 'ASC', + }, + cache: { + id: 'meta_emojis', + milliseconds: 3600000, // 1 hour + }, + }); - name: instance.name, - uri: config.url, - description: instance.description, - langs: instance.langs, - tosUrl: instance.ToSUrl, - 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, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl, - bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - emojis: await Emojis.packMany(emojis), - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - ads: ads.map(ad => ({ - id: ad.id, - url: ad.url, - place: ad.place, - ratio: ad.ratio, - imageUrl: ad.imageUrl, - })), - enableEmail: instance.enableEmail, + const ads = await this.adsRepository.find({ + where: { + expiresAt: MoreThan(new Date()), + }, + }); - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, + const response: any = { + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, - enableServiceWorker: instance.enableServiceWorker, + version: this.config.version, - translatorAvailable: instance.deeplAuthKey != null, + name: instance.name, + uri: this.config.url, + description: instance.description, + langs: instance.langs, + tosUrl: instance.ToSUrl, + 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, + hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableRecaptcha: instance.enableRecaptcha, + recaptchaSiteKey: instance.recaptchaSiteKey, + swPublickey: instance.swPublicKey, + themeColor: instance.themeColor, + mascotImageUrl: instance.mascotImageUrl, + bannerUrl: instance.bannerUrl, + errorImageUrl: instance.errorImageUrl, + iconUrl: instance.iconUrl, + backgroundImageUrl: instance.backgroundImageUrl, + logoImageUrl: instance.logoImageUrl, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため + emojis: await this.emojiEntityService.packMany(emojis), + defaultLightTheme: instance.defaultLightTheme, + defaultDarkTheme: instance.defaultDarkTheme, + ads: ads.map(ad => ({ + id: ad.id, + url: ad.url, + place: ad.place, + ratio: ad.ratio, + imageUrl: ad.imageUrl, + })), + enableEmail: instance.enableEmail, - ...(ps.detail ? { - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - requireSetup: (await Users.countBy({ - host: IsNull(), - })) === 0, - } : {}), - }; + enableTwitterIntegration: instance.enableTwitterIntegration, + enableGithubIntegration: instance.enableGithubIntegration, + enableDiscordIntegration: instance.enableDiscordIntegration, - if (ps.detail) { - const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null; + enableServiceWorker: instance.enableServiceWorker, - response.proxyAccountName = proxyAccount ? proxyAccount.username : null; - response.features = { - registration: !instance.disableRegistration, - localTimeLine: !instance.disableLocalTimeline, - globalTimeLine: !instance.disableGlobalTimeline, - emailRequiredForSignup: instance.emailRequiredForSignup, - elasticsearch: config.elasticsearch ? true : false, - hcaptcha: instance.enableHcaptcha, - recaptcha: instance.enableRecaptcha, - objectStorage: instance.useObjectStorage, - twitter: instance.enableTwitterIntegration, - github: instance.enableGithubIntegration, - discord: instance.enableDiscordIntegration, - serviceWorker: instance.enableServiceWorker, - miauth: true, - }; - } + translatorAvailable: instance.deeplAuthKey != null, + + ...(ps.detail ? { + pinnedPages: instance.pinnedPages, + pinnedClipId: instance.pinnedClipId, + cacheRemoteFiles: instance.cacheRemoteFiles, + requireSetup: (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0, + } : {}), + }; - return response; -}); + if (ps.detail) { + const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; + + 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, + recaptcha: instance.enableRecaptcha, + objectStorage: instance.useObjectStorage, + twitter: instance.enableTwitterIntegration, + github: instance.enableGithubIntegration, + discord: instance.enableDiscordIntegration, + serviceWorker: instance.enableServiceWorker, + miauth: true, + }; + } + + return response; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index 73ecdaeb03..d8eb89c0e6 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -1,7 +1,9 @@ -import define from '../../define.js'; -import { AccessTokens } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['auth'], @@ -37,28 +39,38 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Generate access token - const accessToken = secureRndstr(32, true); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, - const now = new Date(); + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + // Generate access token + const accessToken = secureRndstr(32, true); - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - session: ps.session, - userId: user.id, - token: accessToken, - hash: accessToken, - name: ps.name, - description: ps.description, - iconUrl: ps.iconUrl, - permission: ps.permission, - }); + const now = new Date(); - return { - token: accessToken, - }; -}); + // Insert access token doc + await this.accessTokensRepository.insert({ + id: this.idService.genId(), + createdAt: now, + lastUsedAt: now, + session: ps.session, + userId: me.id, + token: accessToken, + hash: accessToken, + name: ps.name, + description: ps.description, + iconUrl: ps.iconUrl, + permission: ps.permission, + }); + + return { + token: accessToken, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 7e857e6731..cbdd001185 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,10 +1,12 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { MutingsRepository } from '@/models/index.js'; +import type { Muting } from '@/models/entities/Muting.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { genId } from '@/misc/gen-id.js'; -import { Mutings, NoteWatchings } from '@/models/index.js'; -import { Muting } from '@/models/entities/muting.js'; -import { publishUserEvent } from '@/services/stream.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account'], @@ -48,47 +50,54 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const muter = user; - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already muting - const exist = await Mutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } - - if (ps.expiresAt && ps.expiresAt <= Date.now()) { - return; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private globalEventService: GlobalEventService, + private getterService: GetterService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const muter = me; + + // 自分自身 + if (me.id === ps.userId) { + throw new ApiError(meta.errors.muteeIsYourself); + } + + // Get mutee + const mutee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check if already muting + const exist = await this.mutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyMuting); + } + + if (ps.expiresAt && ps.expiresAt <= Date.now()) { + return; + } + + // Create mute + await this.mutingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, + muterId: muter.id, + muteeId: mutee.id, + } as Muting); + + this.globalEventService.publishUserEvent(me.id, 'mute', mutee); + }); } - - // Create mute - await Mutings.insert({ - id: genId(), - createdAt: new Date(), - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, - muterId: muter.id, - muteeId: mutee.id, - } as Muting); - - publishUserEvent(user.id, 'mute', mutee); - - NoteWatchings.delete({ - userId: muter.id, - noteUserId: mutee.id, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index 0b173dbe24..c7098059d5 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MutingsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { Mutings } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['account'], @@ -41,34 +43,45 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const muter = user; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, - // Check if the mutee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } + private globalEventService: GlobalEventService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const muter = me; - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + // Check if the mutee is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.muteeIsYourself); + } - // Check not muting - const exist = await Mutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); + // Get mutee + const mutee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } + // Check not muting + const exist = await this.mutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, + }); - // Delete mute - await Mutings.delete({ - id: exist.id, - }); + if (exist == null) { + throw new ApiError(meta.errors.notMuting); + } - publishUserEvent(user.id, 'unmute', mutee); -}); + // Delete mute + await this.mutingsRepository.delete({ + id: exist.id, + }); + + this.globalEventService.publishUserEvent(me.id, 'unmute', mutee); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 31283cf4c1..11c05eb795 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Mutings } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MutingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { MutingEntityService } from '@/core/entities/MutingEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId) - .andWhere(`muting.muterId = :meId`, { meId: me.id }); - - const mutings = await query - .take(ps.limit) - .getMany(); - - return await Mutings.packMany(mutings, me); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private mutingEntityService: MutingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.mutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + .andWhere('muting.muterId = :meId', { meId: me.id }); + + const mutings = await query + .take(ps.limit) + .getMany(); + + return await this.mutingEntityService.packMany(mutings, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index 85b75c15df..90cd53a133 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { Apps } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AppsRepository } from '@/models/index.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'app'], @@ -27,18 +30,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = { - userId: user.id, - }; - - const apps = await Apps.find({ - where: query, - take: ps.limit, - skip: ps.offset, - }); - - return await Promise.all(apps.map(app => Apps.pack(app, user, { - detail: true, - }))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private appEntityService: AppEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = { + userId: me.id, + }; + + const apps = await this.appsRepository.find({ + where: query, + take: ps.limit, + skip: ps.offset, + }); + + return await Promise.all(apps.map(app => this.appEntityService.pack(app, me, { + detail: true, + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 015b0338e3..288e195316 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,6 +1,9 @@ -import { Notes } from '@/models/index.js'; -import define from '../define.js'; -import { makePaginationQuery } from '../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -32,48 +35,59 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.visibility = \'public\'') - .andWhere('note.localOnly = FALSE') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - if (ps.local) { - query.andWhere('note.userHost IS NULL'); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.visibility = \'public\'') + .andWhere('note.localOnly = FALSE') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + if (ps.local) { + query.andWhere('note.userHost IS NULL'); + } + + if (ps.reply !== undefined) { + query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); + } + + if (ps.renote !== undefined) { + query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); + } + + if (ps.withFiles !== undefined) { + query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); + } + + if (ps.poll !== undefined) { + query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); + } + + // TODO + //if (bot != undefined) { + // query.isBot = bot; + //} + + const notes = await query.take(ps.limit).getMany(); + + return await this.noteEntityService.packMany(notes); + }); } - - if (ps.reply !== undefined) { - query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); - } - - if (ps.renote !== undefined) { - query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); - } - - if (ps.withFiles !== undefined) { - query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); - } - - if (ps.poll !== undefined) { - query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); - } - - // TODO - //if (bot != undefined) { - // query.isBot = bot; - //} - - const notes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes); -}); +} diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index efc109105c..86f90e049f 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,10 +1,10 @@ import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -34,38 +34,49 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where('note.replyId = :noteId', { noteId: ps.noteId }) - .orWhere(new Brackets(qb => { qb - .where('note.renoteId = :noteId', { noteId: ps.noteId }) +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { qb - .where('note.text IS NOT NULL') - .orWhere('note.fileIds != \'{}\'') - .orWhere('note.hasPoll = TRUE'); - })); - })); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + .where('note.replyId = :noteId', { noteId: ps.noteId }) + .orWhere(new Brackets(qb => { qb + .where('note.renoteId = :noteId', { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { qb + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); + })); + })); + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - generateVisibilityQuery(query, user); - if (user) { - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - } + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } - const notes = await query.take(ps.limit).getMany(); + const notes = await query.take(ps.limit).getMany(); - return await Notes.packMany(notes, user); -}); + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index e79f8563e8..7d893f32a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,8 +1,11 @@ import { In } from 'typeorm'; -import { ClipNotes, Clips } from '@/models/index.js'; -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['clips', 'notes'], @@ -37,20 +40,34 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const clipNotes = await ClipNotes.findBy({ - noteId: note.id, - }); - - const clips = await Clips.findBy({ - id: In(clipNotes.map(x => x.clipId)), - isPublic: true, - }); - - return await Promise.all(clips.map(x => Clips.pack(x))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private clipEntityService: ClipEntityService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + const clipNotes = await this.clipNotesRepository.findBy({ + noteId: note.id, + }); + + const clips = await this.clipsRepository.findBy({ + id: In(clipNotes.map(x => x.clipId)), + isPublic: true, + }); + + return await Promise.all(clips.map(x => this.clipEntityService.pack(x))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index b731d18248..2f8324ed62 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,8 +1,11 @@ -import { Note } from '@/models/entities/note.js'; -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { Note } from '@/models/entities/Note.js'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getNote } from '../../common/getters.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -39,36 +42,47 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const conversation: Note[] = []; - let i = 0; + private noteEntityService: NoteEntityService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); - async function get(id: any) { - i++; - const p = await Notes.findOneBy({ id }); - if (p == null) return; + const conversation: Note[] = []; + let i = 0; - if (i > ps.offset!) { - conversation.push(p); - } + const get = async (id: any) => { + i++; + const p = await this.notesRepository.findOneBy({ id }); + if (p == null) return; - if (conversation.length === ps.limit) { - return; - } + if (i > ps.offset!) { + conversation.push(p); + } - if (p.replyId) { - await get(p.replyId); - } - } + if (conversation.length === ps.limit) { + return; + } - if (note.replyId) { - await get(note.replyId); - } + if (p.replyId) { + await get(p.replyId); + } + }; - return await Notes.packMany(conversation, user); -}); + if (note.replyId) { + await get(note.replyId); + } + + return await this.noteEntityService.packMany(conversation, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index a133294169..30b7a889fc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,15 +1,18 @@ import ms from 'ms'; import { In } from 'typeorm'; -import create from '@/services/note/create.js'; -import { User } from '@/models/entities/user.js'; -import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { Channel } from '@/models/entities/channel.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { User } from '@/models/entities/User.js'; +import { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { Channel } from '@/models/entities/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { DI } from '@/di-symbols.js'; import { noteVisibilities } from '../../../../types.js'; import { ApiError } from '../../error.js'; -import define from '../../define.js'; export const meta = { tags: ['notes'], @@ -161,115 +164,138 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let visibleUsers: User[] = []; - if (ps.visibleUserIds) { - visibleUsers = await Users.findBy({ - id: In(ps.visibleUserIds), - }); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - let files: DriveFile[] = []; - const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; - if (fileIds != null) { - files = await DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId AND file.id IN (:...fileIds)', { - userId: user.id, - fileIds, - }) - .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') - .setParameters({ fileIds }) - .getMany(); - } + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - let renote: Note | null = null; - if (ps.renoteId != null) { - // Fetch renote to note - renote = await Notes.findOneBy({ id: ps.renoteId }); + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, - if (renote == null) { - throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { - throw new ApiError(meta.errors.cannotReRenote); - } + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - // Check blocking - if (renote.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: renote.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private noteEntityService: NoteEntityService, + private noteCreateService: NoteCreateService, + ) { + super(meta, paramDef, async (ps, me) => { + let visibleUsers: User[] = []; + if (ps.visibleUserIds) { + visibleUsers = await this.usersRepository.findBy({ + id: In(ps.visibleUserIds), + }); } - } - } - let reply: Note | null = null; - if (ps.replyId != null) { - // Fetch reply - reply = await Notes.findOneBy({ id: ps.replyId }); + let files: DriveFile[] = []; + const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + } - if (reply == null) { - throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } + let renote: Note | null = null; + if (ps.renoteId != null) { + // Fetch renote to note + renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); - // Check blocking - if (reply.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: reply.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); + if (renote == null) { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { + throw new ApiError(meta.errors.cannotReRenote); + } + + // Check blocking + if (renote.userId !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: renote.userId, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } } - } - } - if (ps.poll) { - if (typeof ps.poll.expiresAt === 'number') { - if (ps.poll.expiresAt < Date.now()) { - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + let reply: Note | null = null; + if (ps.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + + if (reply == null) { + throw new ApiError(meta.errors.noSuchReplyTarget); + } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } + + // Check blocking + if (reply.userId !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: reply.userId, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } } - } else if (typeof ps.poll.expiredAfter === 'number') { - ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; - } - } - let channel: Channel | null = null; - if (ps.channelId != null) { - channel = await Channels.findOneBy({ id: ps.channelId }); + if (ps.poll) { + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; + } + } - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - } + let channel: Channel | null = null; + if (ps.channelId != null) { + channel = await this.channelsRepository.findOneBy({ id: ps.channelId }); - // 投稿を作成 - const note = await create(user, { - createdAt: new Date(), - files: files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple || false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text || undefined, - reply, - renote, - cw: ps.cw, - localOnly: ps.localOnly, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - }); + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + } + + // 投稿を作成 + const note = await this.noteCreateService.create(me, { + createdAt: new Date(), + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple || false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + text: ps.text ?? undefined, + reply, + renote, + cw: ps.cw, + localOnly: ps.localOnly, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }); - return { - createdNote: await Notes.pack(note, user), - }; -}); + return { + createdNote: await this.noteEntityService.pack(note, me), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index c23ceeb5bf..4769c8bdf1 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,9 +1,11 @@ import ms from 'ms'; -import deleteNote from '@/services/note/delete.js'; -import { Users } from '@/models/index.js'; -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { 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 '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -42,16 +44,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private getterService: GetterService, + private noteDeleteService: NoteDeleteService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); - // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - await deleteNote(await Users.findOneByOrFail({ id: note.userId }), note); -}); + if ((!me.isAdmin && !me.isModerator) && (note.userId !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため + await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 097371a425..bfdd1acd22 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,8 +1,10 @@ -import { NoteFavorites } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NoteFavoritesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getNote } from '../../../common/getters.js'; export const meta = { tags: ['notes', 'favorites'], @@ -35,28 +37,39 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFavorited); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + private idService: IdService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get favoritee + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + // if already favorited + const exist = await this.noteFavoritesRepository.findOneBy({ + noteId: note.id, + userId: me.id, + }); - // Create favorite - await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); + if (exist != null) { + throw new ApiError(meta.errors.alreadyFavorited); + } + + // Create favorite + await this.noteFavoritesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + noteId: note.id, + userId: me.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 82ef4fa197..6b3a02b101 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,7 +1,9 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { NoteFavoritesRepository } from '@/models/index.js'; import { ApiError } from '../../../error.js'; -import { getNote } from '../../../common/getters.js'; export const meta = { tags: ['notes', 'favorites'], @@ -34,23 +36,33 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFavorited); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get favoritee + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + // if already favorited + const exist = await this.noteFavoritesRepository.findOneBy({ + noteId: note.id, + userId: me.id, + }); - // Delete favorite - await NoteFavorites.delete(exist.id); -}); + if (exist == null) { + throw new ApiError(meta.errors.notFavorited); + } + + // Delete favorite + await this.noteFavoritesRepository.delete(exist.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index dd9cc581aa..9985f9d257 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,7 +1,9 @@ -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -29,39 +31,50 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const max = 30; - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const query = Notes.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere('note.score > 0') - .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) - .andWhere('note.visibility = \'public\'') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const max = 30; + const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); + const query = this.notesRepository.createQueryBuilder('note') + .addSelect('note.score') + .where('note.userHost IS NULL') + .andWhere('note.score > 0') + .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) + .andWhere('note.visibility = \'public\'') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - let notes = await query - .orderBy('note.score', 'DESC') - .take(max) - .getMany(); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + let notes = await query + .orderBy('note.score', 'DESC') + .take(max) + .getMany(); - notes = notes.slice(ps.offset, ps.offset + ps.limit); + notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - return await Notes.packMany(notes, user); -}); + notes = notes.slice(ps.offset, ps.offset + ps.limit); + + return await this.noteEntityService.packMany(notes, me); + }); + } +} 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 925318f544..73b5afa40a 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,13 +1,12 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +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 { ApiError } from '../../error.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateRepliesQuery } from '../../common/generate-replies-query.js'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { tags: ['notes'], @@ -49,50 +48,63 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableGlobalTimeline) { - if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.gtlDisabled); - } - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.visibility = \'public\'') - .andWhere('note.channelId IS NULL') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private metaService: MetaService, + 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); + } + } - generateRepliesQuery(query, user); - if (user) { - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - } + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion + this.queryService.generateRepliesQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } - const timeline = await query.take(ps.limit).getMany(); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); + const timeline = await query.take(ps.limit).getMany(); - return await Notes.packMany(timeline, user); -}); + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(timeline, me); + }); + } +} 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 2dc98c4c9f..c6458223eb 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,16 +1,13 @@ import { Brackets } from 'typeorm'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Followings, Notes } from '@/models/index.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +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 { ApiError } from '../../error.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateRepliesQuery } from '../../common/generate-replies-query.js'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; -import { generateChannelQuery } from '../../common/generate-channel-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { tags: ['notes'], @@ -57,83 +54,99 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.stlDisabled); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private metaService: MetaService, + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + const m = await this.metaService.fetch(); + if (m.disableLocalTimeline && (!me.isAdmin && !me.isModerator)) { + throw new ApiError(meta.errors.stlDisabled); + } + + //#region Construct query + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { + qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) + .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .setParameters(followingQuery.getParameters()); + + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, me); + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); + }); } - - //#region Construct query - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(new Brackets(qb => { - qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) - .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - activeUsersChart.read(user); - }); - - return await Notes.packMany(timeline, user); -}); +} 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 aac2a3749c..7b8859639d 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,16 +1,14 @@ import { Brackets } from 'typeorm'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes, Users } from '@/models/index.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +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 { ApiError } from '../../error.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateRepliesQuery } from '../../common/generate-replies-query.js'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; -import { generateChannelQuery } from '../../common/generate-channel-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { tags: ['notes'], @@ -56,64 +54,77 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableLocalTimeline) { - if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.ltlDisabled); - } - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private metaService: MetaService, + 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); + } + } - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateMutedNoteQuery(query, user); - if (user) generateBlockedUserQuery(query, user); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, me); + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateMutedNoteQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } - })); - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } - //#endregion + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); - const timeline = await query.take(ps.limit).getMany(); + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + } + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); - return await Notes.packMany(timeline, user); -}); + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 9b41544523..9b2dabc88b 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,12 +1,12 @@ import { Brackets } from 'typeorm'; -import read from '@/services/note/read.js'; -import { Notes, Followings } from '@/models/index.js'; -import define from '../../define.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; -import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -37,45 +37,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(`'{"${user.id}"}' <@ note.mentions`) - .orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteThreadQuery(query, user); - generateBlockedUserQuery(query, user); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private noteReadService: NoteReadService, + ) { + super(meta, paramDef, async (ps, me) => { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - if (ps.visibility) { - query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); - } + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(`'{"${me.id}"}' <@ note.mentions`) + .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - if (ps.following) { - query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }); - query.setParameters(followingQuery.getParameters()); - } + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + + if (ps.visibility) { + query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); + } - const mentions = await query.take(ps.limit).getMany(); + if (ps.following) { + query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }); + query.setParameters(followingQuery.getParameters()); + } - read(user.id, mentions); + const mentions = await query.take(ps.limit).getMany(); - return await Notes.packMany(mentions, user); -}); + this.noteReadService.read(me.id, mentions); + + return await this.noteEntityService.packMany(mentions, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 5a04d68f3e..11bfdbba0f 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,6 +1,9 @@ import { Brackets, In } from 'typeorm'; -import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -28,56 +31,75 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = Polls.createQueryBuilder('poll') - .where('poll.userHost IS NULL') - .andWhere('poll.userId != :meId', { meId: user.id }) - .andWhere('poll.noteVisibility = \'public\'') - .andWhere(new Brackets(qb => { qb - .where('poll.expiresAt IS NULL') - .orWhere('poll.expiresAt > :now', { now: new Date() }); - })); - - //#region exclude arleady voted polls - const votedQuery = PollVotes.createQueryBuilder('vote') - .select('vote.noteId') - .where('vote.userId = :meId', { meId: user.id }); - - query - .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`); - - query.setParameters(votedQuery.getParameters()); - //#endregion - - //#region mute - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: user.id }); - - query - .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); - - query.setParameters(mutingQuery.getParameters()); - //#endregion - - const polls = await query - .orderBy('poll.noteId', 'DESC') - .take(ps.limit) - .skip(ps.offset) - .getMany(); - - if (polls.length === 0) return []; - - const notes = await Notes.find({ - where: { - id: In(polls.map(poll => poll.noteId)), - }, - order: { - createdAt: 'DESC', - }, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private noteEntityService: NoteEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.pollsRepository.createQueryBuilder('poll') + .where('poll.userHost IS NULL') + .andWhere('poll.userId != :meId', { meId: me.id }) + .andWhere('poll.noteVisibility = \'public\'') + .andWhere(new Brackets(qb => { qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); + })); + + //#region exclude arleady voted polls + const votedQuery = this.pollVotesRepository.createQueryBuilder('vote') + .select('vote.noteId') + .where('vote.userId = :meId', { meId: me.id }); + + query + .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`); + + query.setParameters(votedQuery.getParameters()); + //#endregion + + //#region mute + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + query + .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); + + query.setParameters(mutingQuery.getParameters()); + //#endregion + + const polls = await query + .orderBy('poll.noteId', 'DESC') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + + if (polls.length === 0) return []; + + const notes = await this.notesRepository.find({ + where: { + id: In(polls.map(poll => poll.noteId)), + }, + order: { + createdAt: 'DESC', + }, + }); - return await Notes.packMany(notes, user, { - detail: true, - }); -}); + return await this.noteEntityService.packMany(notes, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 45a832cbd2..76f07528d7 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,16 +1,17 @@ import { Not } from 'typeorm'; -import { publishNoteStream } from '@/services/stream.js'; -import { createNotification } from '@/services/create-notification.js'; -import { deliver } from '@/queue/index.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderVote from '@/remote/activitypub/renderer/vote.js'; -import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; -import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js'; -import { IRemoteUser } from '@/models/entities/user.js'; -import { genId } from '@/misc/gen-id.js'; -import { getNote } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { IRemoteUser } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { PollService } from '@/core/PollService.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import define from '../../../define.js'; export const meta = { tags: ['notes'], @@ -67,103 +68,116 @@ export const paramDef = { required: ['noteId', 'choice'], } as const; -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const createdAt = new Date(); +// TODO: ロジックをサービスに切り出す - // Get votee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + private idService: IdService, + private getterService: GetterService, + private queueService: QueueService, + private pollService: PollService, + private apRendererService: ApRendererService, + private globalEventService: GlobalEventService, + private createNotificationService: CreateNotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + const createdAt = new Date(); + + // Get votee + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); - if (!note.hasPoll) { - throw new ApiError(meta.errors.noPoll); - } + if (!note.hasPoll) { + throw new ApiError(meta.errors.noPoll); + } - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } + // Check blocking + if (note.userId !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: note.userId, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } - const poll = await Polls.findOneByOrFail({ noteId: note.id }); + const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); - if (poll.expiresAt && poll.expiresAt < createdAt) { - throw new ApiError(meta.errors.alreadyExpired); - } + if (poll.expiresAt && poll.expiresAt < createdAt) { + throw new ApiError(meta.errors.alreadyExpired); + } - if (poll.choices[ps.choice] == null) { - throw new ApiError(meta.errors.invalidChoice); - } + if (poll.choices[ps.choice] == null) { + throw new ApiError(meta.errors.invalidChoice); + } - // if already voted - const exist = await PollVotes.findBy({ - noteId: note.id, - userId: user.id, - }); + // if already voted + const exist = await this.pollVotesRepository.findBy({ + noteId: note.id, + userId: me.id, + }); - if (exist.length) { - if (poll.multiple) { - if (exist.some(x => x.choice === ps.choice)) { - throw new ApiError(meta.errors.alreadyVoted); + if (exist.length) { + if (poll.multiple) { + if (exist.some(x => x.choice === ps.choice)) { + throw new ApiError(meta.errors.alreadyVoted); + } + } else { + throw new ApiError(meta.errors.alreadyVoted); + } } - } else { - throw new ApiError(meta.errors.alreadyVoted); - } - } - // Create vote - const vote = await PollVotes.insert({ - id: genId(), - createdAt, - noteId: note.id, - userId: user.id, - choice: ps.choice, - }).then(x => PollVotes.findOneByOrFail(x.identifiers[0])); - - // Increment votes count - const index = ps.choice + 1; // In SQL, array index is 1 based - await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - - publishNoteStream(note.id, 'pollVoted', { - choice: ps.choice, - userId: user.id, - }); - - // Notify - createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: ps.choice, - }); - - // Fetch watchers - NoteWatchings.findBy({ - noteId: note.id, - userId: Not(user.id), - }).then(watchers => { - for (const watcher of watchers) { - createNotification(watcher.userId, 'pollVote', { - notifierId: user.id, + // Create vote + const vote = await this.pollVotesRepository.insert({ + id: this.idService.genId(), + createdAt, noteId: note.id, + userId: me.id, choice: ps.choice, + }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0])); + + // Increment votes count + const index = ps.choice + 1; // In SQL, array index is 1 based + await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); + + this.globalEventService.publishNoteStream(note.id, 'pollVoted', { + choice: ps.choice, + userId: me.id, }); - } - }); - // リモート投票の場合リプライ送信 - if (note.userHost != null) { - const pollOwner = await Users.findOneByOrFail({ id: note.userId }) as IRemoteUser; + // Notify + this.createNotificationService.createNotification(note.userId, 'pollVote', { + notifierId: me.id, + noteId: note.id, + choice: ps.choice, + }); - deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox); - } + // リモート投票の場合リプライ送信 + if (note.userHost != null) { + const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; + + this.queueService.deliver(me, this.apRendererService.renderActivity(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); + } - // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(note.id); -}); + // リモートフォロワーにUpdate配信 + this.pollService.deliverQuestionUpdate(note.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 15a62d394d..d57950f012 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,8 +1,12 @@ -import { DeepPartial, FindOptionsWhere } from 'typeorm'; -import { NoteReactions } from '@/models/index.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import define from '../../define.js'; +import { DeepPartial } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { NoteReactionsRepository } from '@/models/index.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import type { FindOptionsWhere } from 'typeorm'; export const meta = { tags: ['notes', 'reactions'], @@ -45,28 +49,38 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = { - noteId: ps.noteId, - } as FindOptionsWhere; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, - if (ps.type) { - // ローカルリアクションはホスト名が . とされているが - // DB 上ではそうではないので、必要に応じて変換 - const suffix = '@.:'; - const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type; - query.reaction = type; - } + private noteReactionEntityService: NoteReactionEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = { + noteId: ps.noteId, + } as FindOptionsWhere; - const reactions = await NoteReactions.find({ - where: query, - take: ps.limit, - skip: ps.offset, - order: { - id: -1, - }, - relations: ['user', 'user.avatar', 'user.banner', 'note'], - }); + if (ps.type) { + // ローカルリアクションはホスト名が . とされているが + // DB 上ではそうではないので、必要に応じて変換 + const suffix = '@.:'; + const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type; + query.reaction = type; + } - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user))); -}); + const reactions = await this.noteReactionsRepository.find({ + where: query, + take: ps.limit, + skip: ps.offset, + order: { + id: -1, + }, + relations: ['user', 'user.avatar', 'user.banner', 'note'], + }); + + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 07e52a9266..2af734307d 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,6 +1,7 @@ -import createReaction from '@/services/note/reaction/create.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { ReactionService } from '@/core/ReactionService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -41,15 +42,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - await createReaction(user, note, ps.reaction).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); - if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); - throw e; - }); - return; -}); +@Injectable() +export default class extends Endpoint { + constructor( + private getterService: GetterService, + private reactionService: ReactionService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + await this.reactionService.create(me, note, ps.reaction).catch(err => { + if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); + if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); + throw err; + }); + return; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index c13cafa21d..31ed962922 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,7 +1,8 @@ import ms from 'ms'; -import deleteReaction from '@/services/note/reaction/delete.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { ReactionService } from '@/core/ReactionService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -41,13 +42,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - await deleteReaction(user, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); - throw e; - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private getterService: GetterService, + private reactionService: ReactionService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + await this.reactionService.delete(me, note).catch(err => { + if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); + throw err; + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 28be360763..57b7aeae0d 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,11 +1,11 @@ -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -43,31 +43,43 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.renoteId = :renoteId', { renoteId: note.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.renoteId = :renoteId', { renoteId: note.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - const renotes = await query.take(ps.limit).getMany(); + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - return await Notes.packMany(renotes, user); -}); + const renotes = await query.take(ps.limit).getMany(); + + return await this.noteEntityService.packMany(renotes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index ab0018f58e..7020d0c681 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,9 +1,9 @@ -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -33,26 +33,37 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - const timeline = await query.take(ps.limit).getMany(); - - return await Notes.packMany(timeline, user); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + + const timeline = await query.take(ps.limit).getMany(); + + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 777de7221c..0727c9af6c 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,12 +1,12 @@ import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes', 'hashtags'], @@ -66,75 +66,86 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - try { - if (ps.tag) { - if (!safeForSql(ps.tag)) throw 'Injection'; - query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); - } else { - query.andWhere(new Brackets(qb => { - for (const tags of ps.query!) { - qb.orWhere(new Brackets(qb => { - for (const tag of tags) { - if (!safeForSql(tag)) throw 'Injection'; - qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + + try { + if (ps.tag) { + if (!safeForSql(ps.tag)) throw 'Injection'; + query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); + } else { + query.andWhere(new Brackets(qb => { + for (const tags of ps.query!) { + qb.orWhere(new Brackets(qb => { + for (const tag of tags) { + if (!safeForSql(tag)) throw 'Injection'; + qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); + } + })); } })); } - })); - } - } catch (e) { - if (e === 'Injection') return []; - throw e; - } + } catch (e) { + if (e === 'Injection') return []; + throw e; + } - if (ps.reply != null) { - if (ps.reply) { - query.andWhere('note.replyId IS NOT NULL'); - } else { - query.andWhere('note.replyId IS NULL'); - } - } + if (ps.reply != null) { + if (ps.reply) { + query.andWhere('note.replyId IS NOT NULL'); + } else { + query.andWhere('note.replyId IS NULL'); + } + } - if (ps.renote != null) { - if (ps.renote) { - query.andWhere('note.renoteId IS NOT NULL'); - } else { - query.andWhere('note.renoteId IS NULL'); - } - } + if (ps.renote != null) { + if (ps.renote) { + query.andWhere('note.renoteId IS NOT NULL'); + } else { + query.andWhere('note.renoteId IS NULL'); + } + } - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } - if (ps.poll != null) { - if (ps.poll) { - query.andWhere('note.hasPoll = TRUE'); - } else { - query.andWhere('note.hasPoll = FALSE'); - } - } + if (ps.poll != null) { + if (ps.poll) { + query.andWhere('note.hasPoll = TRUE'); + } else { + query.andWhere('note.hasPoll = FALSE'); + } + } - // Search notes - const notes = await query.take(ps.limit).getMany(); + // Search notes + const notes = await query.take(ps.limit).getMany(); - return await Notes.packMany(notes, me); -}); + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 4e2cdae801..484cfc1128 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,12 +1,11 @@ import { In } from 'typeorm'; -import { Notes } from '@/models/index.js'; -import config from '@/config/index.js'; -import es from '../../../../db/elasticsearch.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -46,97 +45,51 @@ export const paramDef = { required: ['query'], } as const; -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - if (es == null) { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId); - - if (ps.userId) { - query.andWhere('note.userId = :userId', { userId: ps.userId }); - } else if (ps.channelId) { - query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); - } - - query - .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes, me); - } else { - const userQuery = ps.userId != null ? [{ - term: { - userId: ps.userId, - }, - }] : []; - - const hostQuery = ps.userId == null ? - ps.host === null ? [{ - bool: { - must_not: { - exists: { - field: 'userHost', - }, - }, - }, - }] : ps.host !== undefined ? [{ - term: { - userHost: ps.host, - }, - }] : [] - : []; +// TODO: ロジックをサービスに切り出す - const result = await es.search({ - index: config.elasticsearch.index || 'misskey_note', - body: { - size: ps.limit, - from: ps.offset, - query: { - bool: { - must: [{ - simple_query_string: { - fields: ['text'], - query: ps.query.toLowerCase(), - default_operator: 'and', - }, - }, ...hostQuery, ...userQuery], - }, - }, - sort: [{ - _doc: 'desc', - }], - }, - }); - - const hits = result.body.hits.hits.map((hit: any) => hit._id); - - if (hits.length === 0) return []; - - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits), - }, - order: { - id: -1, - }, +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); + + if (ps.userId) { + query.andWhere('note.userId = :userId', { userId: ps.userId }); + } else if (ps.channelId) { + query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); + } + + query + .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + + const notes = await query.take(ps.limit).getMany(); + + return await this.noteEntityService.packMany(notes, me); }); - - return await Notes.packMany(notes, me); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 5cd74bd2ca..c3f5b9dfb0 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,7 +1,10 @@ -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -32,13 +35,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - return await Notes.pack(note, user, { - detail: true, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + return await this.noteEntityService.pack(note, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 01afa5add2..7756d39f7c 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,7 @@ -import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -35,36 +37,42 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await Notes.findOneByOrFail({ id: ps.noteId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - const [favorite, watching, threadMuting] = await Promise.all([ - NoteFavorites.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), - NoteWatchings.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), - NoteThreadMutings.count({ - where: { - userId: user.id, - threadId: note.threadId || note.id, - }, - take: 1, - }), - ]); + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId }); + + const [favorite, threadMuting] = await Promise.all([ + this.noteFavoritesRepository.count({ + where: { + userId: me.id, + noteId: note.id, + }, + take: 1, + }), + this.noteThreadMutingsRepository.count({ + where: { + userId: me.id, + threadId: note.threadId || note.id, + }, + take: 1, + }), + ]); - return { - isFavorited: favorite !== 0, - isWatching: watching !== 0, - isMutedThread: threadMuting !== 0, - }; -}); + return { + isFavorited: favorite !== 0, + isMutedThread: threadMuting !== 0, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index cf360526d3..1c83adddff 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,8 +1,10 @@ -import { Notes, NoteThreadMutings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import readNote from '@/services/note/read.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -30,26 +32,41 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const mutedNotes = await Notes.find({ - where: [{ - id: note.threadId || note.id, - }, { - threadId: note.threadId || note.id, - }], - }); - - await readNote(user.id, mutedNotes); - - await NoteThreadMutings.insert({ - id: genId(), - createdAt: new Date(), - threadId: note.threadId || note.id, - userId: user.id, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + private getterService: GetterService, + private noteReadService: NoteReadService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + const mutedNotes = await this.notesRepository.find({ + where: [{ + id: note.threadId || note.id, + }, { + threadId: note.threadId || note.id, + }], + }); + + await this.noteReadService.read(me.id, mutedNotes); + + await this.noteThreadMutingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + threadId: note.threadId || note.id, + userId: me.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index ac310d0fe6..1f896734d1 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,6 +1,8 @@ -import { NoteThreadMutings } from '@/models/index.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NoteThreadMutingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,14 +30,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await NoteThreadMutings.delete({ - threadId: note.threadId || note.id, - userId: user.id, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + await this.noteThreadMutingsRepository.delete({ + threadId: note.threadId || note.id, + userId: me.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 22f4925175..53a1ae1348 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,14 +1,12 @@ import { Brackets } from 'typeorm'; -import { Notes, Followings } from '@/models/index.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateRepliesQuery } from '../../common/generate-replies-query.js'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; -import { generateChannelQuery } from '../../common/generate-channel-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -47,85 +45,100 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const hasFollowing = (await Followings.count({ - where: { - followerId: user.id, - }, - take: 1, - })) !== 0; - - //#region Construct query - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(new Brackets(qb => { qb - .where('note.userId = :meId', { meId: user.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + const hasFollowing = (await this.followingsRepository.count({ + where: { + followerId: me.id, + }, + take: 1, + })) !== 0; - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion + //#region Construct query + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { qb + .where('note.userId = :meId', { meId: me.id }); + if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .setParameters(followingQuery.getParameters()); - const timeline = await query.take(ps.limit).getMany(); + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, me); + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); - process.nextTick(() => { - activeUsersChart.read(user); - }); + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - return await Notes.packMany(timeline, user); -}); + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 5e40e7106f..c24f1e401e 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,12 +1,15 @@ import { URLSearchParams } from 'node:url'; import fetch from 'node-fetch'; -import config from '@/config/index.js'; -import { getAgentByUrl } from '@/misc/fetch.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; import { ApiError } from '../../error.js'; -import { getNote } from '../../common/getters.js'; -import define from '../../define.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -37,58 +40,74 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) { - return 204; // TODO: 良い感じのエラー返す - } - - if (note.text == null) { - return 204; - } - - const instance = await fetchMeta(); - - if (instance.deeplAuthKey == null) { - return 204; // TODO: 良い感じのエラー返す - } - - let targetLang = ps.targetLang; - if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; - - const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); - - const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; - - const res = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': config.userAgent, - Accept: 'application/json, */*', - }, - body: params, - // TODO - //timeout: 10000, - agent: getAgentByUrl, - }); - - const json = (await res.json()) as { +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private getterService: GetterService, + private metaService: MetaService, + private httpRequestService: HttpRequestService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (!(await this.noteEntityService.isVisibleForMe(note, me ? me.id : null))) { + return 204; // TODO: 良い感じのエラー返す + } + + if (note.text == null) { + return 204; + } + + const instance = await this.metaService.fetch(); + + if (instance.deeplAuthKey == null) { + return 204; // TODO: 良い感じのエラー返す + } + + let targetLang = ps.targetLang; + if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; + + const params = new URLSearchParams(); + params.append('auth_key', instance.deeplAuthKey); + params.append('text', note.text); + params.append('target_lang', targetLang); + + const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': config.userAgent, + Accept: 'application/json, */*', + }, + body: params, + // TODO + //timeout: 10000, + agent: (url) => this.httpRequestService.getAgentByUrl(url), + }); + + const json = (await res.json()) as { translations: { detected_source_language: string; text: string; }[]; }; - return { - sourceLang: json.translations[0].detected_source_language, - text: json.translations[0].text, - }; -}); + return { + sourceLang: json.translations[0].detected_source_language, + text: json.translations[0].text, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 3fba0efe0c..c0048888b4 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,9 +1,11 @@ import ms from 'ms'; -import deleteNote from '@/services/note/delete.js'; -import { Notes, Users } from '@/models/index.js'; -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, NotesRepository } 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 '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -36,18 +38,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const renotes = await Notes.findBy({ - userId: user.id, - renoteId: note.id, - }); - - for (const note of renotes) { - deleteNote(await Users.findOneByOrFail({ id: user.id }), note); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private getterService: GetterService, + private noteDeleteService: NoteDeleteService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + const renotes = await this.notesRepository.findBy({ + userId: me.id, + renoteId: note.id, + }); + + for (const note of renotes) { + this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: me.id }), note); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index e603a8f625..87a464578c 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,10 +1,12 @@ import { Brackets } from 'typeorm'; -import { UserLists, UserListJoinings, Notes } from '@/models/index.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; export const meta = { tags: ['notes', 'lists'], @@ -52,72 +54,90 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const list = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (list == null) { - throw new ApiError(meta.errors.noSuchList); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (list == null) { + throw new ApiError(meta.errors.noSuchList); + } + + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); + + this.queryService.generateVisibilityQuery(query, me); + + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); + + this.activeUsersChart.read(me); + + return await this.noteEntityService.packMany(timeline, me); + }); } - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(UserListJoinings.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); - - generateVisibilityQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - activeUsersChart.read(user); - - return await Notes.packMany(timeline, user); -}); +} diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts deleted file mode 100644 index 7d482b0732..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ /dev/null @@ -1,38 +0,0 @@ -import watch from '@/services/note/watch.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await watch(user.id, note); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts deleted file mode 100644 index 2c1a2e5fbd..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ /dev/null @@ -1,38 +0,0 @@ -import unwatch from '@/services/note/unwatch.js'; -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '09b3695c-f72c-4731-a428-7cff825fc82e', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await unwatch(user.id, note); -}); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 80d513d8da..3427a3eb5c 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,5 +1,6 @@ -import { createNotification } from '@/services/create-notification.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; export const meta = { tags: ['notifications'], @@ -23,11 +24,18 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, token) => { - createNotification(user.id, 'app', { - appAccessTokenId: token ? token.id : null, - customBody: ps.body, - customHeader: ps.header, - customIcon: ps.icon, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private createNotificationService: CreateNotificationService, + ) { + super(meta, paramDef, async (ps, user, token) => { + this.createNotificationService.createNotification(user.id, 'app', { + appAccessTokenId: token ? token.id : null, + customBody: ps.body, + customHeader: ps.header, + customIcon: ps.icon, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index d169afbb35..3d1eb2b39c 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,7 +1,9 @@ -import { publishMainStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { Notifications } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotificationsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notifications', 'account'], @@ -18,16 +20,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Update documents - await Notifications.update({ - notifieeId: user.id, - isRead: false, - }, { - isRead: true, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, - // 全ての通知を読みましたよというイベントを発行 - publishMainStream(user.id, 'readAllNotifications'); - pushNotification(user.id, 'readAllNotifications', undefined); -}); + private globalEventService: GlobalEventService, + private pushNotificationService: PushNotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + // Update documents + await this.notificationsRepository.update({ + notifieeId: me.id, + isRead: false, + }, { + isRead: true, + }); + + // 全ての通知を読みましたよというイベントを発行 + this.globalEventService.publishMainStream(me.id, 'readAllNotifications'); + this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index 7bce525a55..cdf8d09f9e 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -1,5 +1,6 @@ -import define from '../../define.js'; -import { readNotification } from '../../common/read-notification.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { tags: ['notifications', 'account'], @@ -43,7 +44,14 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]); - return readNotification(user.id, ps.notificationIds); -}); +@Injectable() +export default class extends Endpoint { + constructor( + private notificationService: NotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + if ('notificationId' in ps) return this.notificationService.readNotification(me.id, [ps.notificationId]); + return this.notificationService.readNotification(me.id, ps.notificationIds); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 6dd3ede85a..1b0299c3c6 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,6 +1,10 @@ -import { publishMainStream } from '@/services/stream.js'; -import { Users, Pages } from '@/models/index.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../error.js'; export const meta = { @@ -27,19 +31,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } - publishMainStream(page.userId, 'pageEvent', { - pageId: ps.pageId, - event: ps.event, - var: ps.var, - userId: user.id, - user: await Users.pack(user.id, { id: page.userId }, { - detail: true, - }), - }); -}); + this.globalEventService.publishMainStream(page.userId, 'pageEvent', { + pageId: ps.pageId, + event: ps.event, + var: ps.var, + userId: me.id, + user: await this.userEntityService.pack(me.id, { id: page.userId }, { + detail: true, + }), + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index b008cde84e..ac80849aa0 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,8 +1,11 @@ import ms from 'ms'; -import { Pages, DriveFiles } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { Page } from '@/models/entities/page.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DriveFilesRepository, PagesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Page } from '@/models/entities/Page.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -59,45 +62,59 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOneBy({ - id: ps.eyeCatchingImageId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private pageEntityService: PageEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + let eyeCatchingImage = null; + if (ps.eyeCatchingImageId != null) { + eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + id: ps.eyeCatchingImageId, + userId: me.id, + }); - await Pages.findBy({ - userId: user.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); + if (eyeCatchingImage == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } - const page = await Pages.insert(new Page({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - name: ps.name, - summary: ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, - userId: user.id, - visibility: 'public', - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - })).then(x => Pages.findOneByOrFail(x.identifiers[0])); + await this.pagesRepository.findBy({ + userId: me.id, + name: ps.name, + }).then(result => { + if (result.length > 0) { + throw new ApiError(meta.errors.nameAlreadyExists); + } + }); - return await Pages.pack(page); -}); + const page = await this.pagesRepository.insert(new Page({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: new Date(), + title: ps.title, + name: ps.name, + summary: ps.summary, + content: ps.content, + variables: ps.variables, + script: ps.script, + eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, + userId: me.id, + visibility: 'public', + alignCenter: ps.alignCenter, + hideTitleWhenPinned: ps.hideTitleWhenPinned, + font: ps.font, + })).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0])); + + return await this.pageEntityService.pack(page); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index a7708e6585..4e97755761 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,5 +1,7 @@ -import { Pages } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -33,14 +35,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + if (page.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await this.pagesRepository.delete(page.id); + }); } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - await Pages.delete(page.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index 5a149a626e..3e3dbb0832 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,8 @@ -import { Pages } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['pages'], @@ -24,13 +27,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Pages.createQueryBuilder('page') - .where('page.visibility = \'public\'') - .andWhere('page.likedCount > 0') - .orderBy('page.likedCount', 'DESC'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - const pages = await query.take(10).getMany(); + private pageEntityService: PageEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.pagesRepository.createQueryBuilder('page') + .where('page.visibility = \'public\'') + .andWhere('page.likedCount > 0') + .orderBy('page.likedCount', 'DESC'); - return await Pages.packMany(pages, me); -}); + const pages = await query.take(10).getMany(); + + return await this.pageEntityService.packMany(pages, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 269b539f74..f3c55fed8b 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,6 +1,8 @@ -import { Pages, PageLikes } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,33 +42,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - if (page.userId === user.id) { - throw new ApiError(meta.errors.yourPage); - } + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, - // if already liked - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, - }); + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } + if (page.userId === me.id) { + throw new ApiError(meta.errors.yourPage); + } - // Create like - await PageLikes.insert({ - id: genId(), - createdAt: new Date(), - pageId: page.id, - userId: user.id, - }); + // if already liked + const exist = await this.pageLikesRepository.findOneBy({ + pageId: page.id, + userId: me.id, + }); - Pages.increment({ id: page.id }, 'likedCount', 1); -}); + if (exist != null) { + throw new ApiError(meta.errors.alreadyLiked); + } + + // Create like + await this.pageLikesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + pageId: page.id, + userId: me.id, + }); + + this.pagesRepository.increment({ id: page.id }, 'likedCount', 1); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 5d37e86b91..6d73889d39 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,7 +1,10 @@ import { IsNull } from 'typeorm'; -import { Pages, Users } from '@/models/index.js'; -import { Page } from '@/models/entities/page.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, PagesRepository } from '@/models/index.js'; +import type { Page } from '@/models/entities/Page.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,27 +47,40 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - let page: Page | null = null; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (ps.pageId) { - page = await Pages.findOneBy({ id: ps.pageId }); - } else if (ps.name && ps.username) { - const author = await Users.findOneBy({ - host: IsNull(), - usernameLower: ps.username.toLowerCase(), - }); - if (author) { - page = await Pages.findOneBy({ - name: ps.name, - userId: author.id, - }); - } - } + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } + private pageEntityService: PageEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + let page: Page | null = null; + + if (ps.pageId) { + page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + } else if (ps.name && ps.username) { + const author = await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: ps.username.toLowerCase(), + }); + if (author) { + page = await this.pagesRepository.findOneBy({ + name: ps.name, + userId: author.id, + }); + } + } - return await Pages.pack(page, user); -}); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + + return await this.pageEntityService.pack(page, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 6b3a2bec10..88386739be 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,5 +1,7 @@ -import { Pages, PageLikes } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -33,23 +35,34 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, - }); + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } + const exist = await this.pageLikesRepository.findOneBy({ + pageId: page.id, + userId: me.id, + }); - // Delete like - await PageLikes.delete(exist.id); + if (exist == null) { + throw new ApiError(meta.errors.notLiked); + } - Pages.decrement({ id: page.id }, 'likedCount', 1); -}); + // Delete like + await this.pageLikesRepository.delete(exist.id); + + this.pagesRepository.decrement({ id: page.id }, 'likedCount', 1); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index d241f585aa..8980ac4906 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,7 +1,9 @@ import ms from 'ms'; import { Not } from 'typeorm'; -import { Pages, DriveFiles } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PagesRepository, DriveFilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -65,52 +67,63 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOneBy({ - id: ps.eyeCatchingImageId, - userId: user.id, - }); + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + if (page.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } + let eyeCatchingImage = null; + if (ps.eyeCatchingImageId != null) { + eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + id: ps.eyeCatchingImageId, + userId: me.id, + }); - await Pages.findBy({ - id: Not(ps.pageId), - userId: user.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); + if (eyeCatchingImage == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } - await Pages.update(page.id, { - updatedAt: new Date(), - title: ps.title, - name: ps.name === undefined ? page.name : ps.name, - summary: ps.name === undefined ? page.summary : ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, - }); -}); + await this.pagesRepository.findBy({ + id: Not(ps.pageId), + userId: me.id, + name: ps.name, + }).then(result => { + if (result.length > 0) { + throw new ApiError(meta.errors.nameAlreadyExists); + } + }); + + await this.pagesRepository.update(page.id, { + updatedAt: new Date(), + title: ps.title, + name: ps.name === undefined ? page.name : ps.name, + summary: ps.name === undefined ? page.summary : ps.summary, + content: ps.content, + variables: ps.variables, + script: ps.script, + alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, + hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, + font: ps.font === undefined ? page.font : ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId === null + ? null + : ps.eyeCatchingImageId === undefined + ? page.eyeCatchingImageId + : eyeCatchingImage!.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index 2891a0860a..4bb62b298e 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -1,4 +1,5 @@ -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { requireCredential: false, @@ -24,8 +25,14 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - return { - pong: Date.now(), - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async () => { + return { + pong: Date.now(), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 41595b47d9..573331e0d8 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,9 +1,12 @@ import { IsNull } from 'typeorm'; -import { Users } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; -import { User } from '@/models/entities/user.js'; -import define from '../define.js'; +import type { User } from '@/models/entities/User.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -28,13 +31,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const meta = await fetchMeta(); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => Users.findOneBy({ - usernameLower: acct.username.toLowerCase(), - host: acct.host ?? IsNull(), - }))); + private metaService: MetaService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const meta = await this.metaService.fetch(); - return await Users.packMany(users.filter(x => x !== undefined) as User[], me, { detail: true }); -}); + const users = await Promise.all(meta.pinnedthis.usersRepository.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ + usernameLower: acct.username.toLowerCase(), + host: acct.host ?? IsNull(), + }))); + + return await this.userEntityService.packMany(users.filter(x => x !== undefined) as User[], me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index c6a940c65e..7c8188ce3c 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,8 +1,10 @@ -import { PromoReads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PromoReadsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getNote } from '../../common/getters.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['notes'], @@ -27,25 +29,36 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const exist = await PromoReads.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist != null) { - return; - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.promoReadsRepository) + private promoReadsRepository: PromoReadsRepository, + + private idService: IdService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + const exist = await this.promoReadsRepository.findOneBy({ + noteId: note.id, + userId: me.id, + }); - await PromoReads.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); + if (exist != null) { + return; + } + + await this.promoReadsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + noteId: note.id, + userId: me.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 511a6bbb53..4766239533 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,13 +1,14 @@ import rndstr from 'rndstr'; import ms from 'ms'; import { IsNull } from 'typeorm'; -import { publishMainStream } from '@/services/stream.js'; -import config from '@/config/index.js'; -import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; -import { sendEmail } from '@/services/send-email.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { EmailService } from '@/core/EmailService.js'; import { ApiError } from '../error.js'; -import define from '../define.js'; export const meta = { tags: ['reset password'], @@ -36,41 +37,61 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ - usernameLower: ps.username.toLowerCase(), - host: IsNull(), - }); - - // 合致するユーザーが登録されていなかったら無視 - if (user == null) { - return; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.passwordResetRequestsRepository) + private passwordResetRequestsRepository: PasswordResetRequestsRepository, + + private idService: IdService, + private emailService: EmailService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ + usernameLower: ps.username.toLowerCase(), + host: IsNull(), + }); + + // 合致するユーザーが登録されていなかったら無視 + if (user == null) { + return; + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + // 合致するメアドが登録されていなかったら無視 + if (profile.email !== ps.email) { + return; + } + + // メアドが認証されていなかったら無視 + if (!profile.emailVerified) { + return; + } + + const token = rndstr('a-z0-9', 64); + + await this.passwordResetRequestsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: profile.userId, + token, + }); + + const link = `${this.config.url}/reset-password/${token}`; + + this.emailService.sendEmail(ps.email, 'Password reset requested', + `To reset password, please click this link:
${link}`, + `To reset password, please click this link: ${link}`); + }); } - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // 合致するメアドが登録されていなかったら無視 - if (profile.email !== ps.email) { - return; - } - - // メアドが認証されていなかったら無視 - if (!profile.emailVerified) { - return; - } - - const token = rndstr('a-z0-9', 64); - - await PasswordResetRequests.insert({ - id: genId(), - createdAt: new Date(), - userId: profile.userId, - token, - }); - - const link = `${config.url}/reset-password/${token}`; - - sendEmail(ps.email, 'Password reset requested', - `To reset password, please click this link:
${link}`, - `To reset password, please click this link: ${link}`); -}); +} diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 140f96d579..526efbc2f6 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,5 +1,9 @@ -import { resetDb } from '@/db/postgre.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import Redis from 'ioredis'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { resetDb } from '@/misc/reset-db.js'; import { ApiError } from '../error.js'; export const meta = { @@ -21,10 +25,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; - - await resetDb(); - - await new Promise(resolve => setTimeout(resolve, 1000)); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + ) { + super(meta, paramDef, async (ps, me) => { + if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; + + await redisClient.flushdb(); + await resetDb(this.db); + + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 797169c2c3..48edde5196 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,7 +1,9 @@ import bcrypt from 'bcryptjs'; -import { publishMainStream } from '@/services/stream.js'; -import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../error.js'; export const meta = { @@ -26,23 +28,34 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const req = await PasswordResetRequests.findOneByOrFail({ - token: ps.token, - }); - - // 発行してから30分以上経過していたら無効 - if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { - throw new Error(); // TODO +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.passwordResetRequestsRepository) + private passwordResetRequestsRepository: PasswordResetRequestsRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const req = await this.passwordResetRequestsRepository.findOneByOrFail({ + token: ps.token, + }); + + // 発行してから30分以上経過していたら無効 + if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { + throw new Error(); // TODO + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.password, salt); + + await this.userProfilesRepository.update(req.userId, { + password: hash, + }); + + this.passwordResetRequestsRepository.delete(req.id); + }); } - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(ps.password, salt); - - await UserProfiles.update(req.userId, { - password: hash, - }); - - PasswordResetRequests.delete(req.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 99f3730e97..8989a3073d 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,6 +1,7 @@ import * as os from 'node:os'; import si from 'systeminformation'; -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { requireCredential: false, @@ -15,22 +16,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async () => { + const memStats = await si.mem(); + const fsStats = await si.fsSize(); - return { - machine: os.hostname(), - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length, - }, - mem: { - total: memStats.total, - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - }; -}); + return { + machine: os.hostname(), + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length, + }, + mem: { + total: memStats.total, + }, + fs: { + total: fsStats[0].size, + used: fsStats[0].used, + }, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index cc94f8bf26..17af75578b 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,7 +1,8 @@ -import { Instances, NoteReactions, Notes, Users } from '@/models/index.js'; -import define from '../define.js'; -import { } from '@/services/chart/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; +import { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: false, @@ -51,34 +52,51 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const [ - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - ] = await Promise.all([ - Notes.count({ cache: 3600000 }), // 1 hour - Notes.count({ where: { userHost: IsNull() }, cache: 3600000 }), - Users.count({ cache: 3600000 }), - Users.count({ where: { host: IsNull() }, cache: 3600000 }), - NoteReactions.count({ cache: 3600000 }), // 1 hour - //NoteReactions.count({ where: { userHost: IsNull() }, cache: 3600000 }), - Instances.count({ cache: 3600000 }), - ]); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - return { - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - driveUsageLocal: 0, - driveUsageRemote: 0, - }; -}); + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + ) { + super(meta, paramDef, async () => { + const [ + notesCount, + originalNotesCount, + usersCount, + originalUsersCount, + reactionsCount, + //originalReactionsCount, + instances, + ] = await Promise.all([ + this.notesRepository.count({ cache: 3600000 }), // 1 hour + this.notesRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }), + this.usersRepository.count({ cache: 3600000 }), + this.usersRepository.count({ where: { host: IsNull() }, cache: 3600000 }), + this.noteReactionsRepository.count({ cache: 3600000 }), // 1 hour + //this.noteReactionsRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }), + this.instancesRepository.count({ cache: 3600000 }), + ]); + + return { + notesCount, + originalNotesCount, + usersCount, + originalUsersCount, + reactionsCount, + //originalReactionsCount, + instances, + driveUsageLocal: 0, + driveUsageRemote: 0, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 437f8874ff..73a084c2ad 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,7 +1,9 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { genId } from '@/misc/gen-id.js'; -import { SwSubscriptions } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { IdService } from '@/core/IdService.js'; +import { SwSubscriptionsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -38,35 +40,46 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // if already subscribed - const exist = await SwSubscriptions.findOneBy({ - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.swSubscriptionsRepository) + private swSubscriptionsRepository: SwSubscriptionsRepository, - const instance = await fetchMeta(true); + private idService: IdService, + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + // if already subscribed + const exist = await this.swSubscriptionsRepository.findOneBy({ + userId: me.id, + endpoint: ps.endpoint, + auth: ps.auth, + publickey: ps.publickey, + }); - if (exist != null) { - return { - state: 'already-subscribed' as const, - key: instance.swPublicKey, - }; - } + const instance = await this.metaService.fetch(true); + + if (exist != null) { + return { + state: 'already-subscribed' as const, + key: instance.swPublicKey, + }; + } - await SwSubscriptions.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); + await this.swSubscriptionsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + endpoint: ps.endpoint, + auth: ps.auth, + publickey: ps.publickey, + }); - return { - state: 'subscribed' as const, - key: instance.swPublicKey, - }; -}); + return { + state: 'subscribed' as const, + key: instance.swPublicKey, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index c19e06b879..feb6730154 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,5 +1,7 @@ -import { SwSubscriptions } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { SwSubscriptionsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -18,9 +20,17 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await SwSubscriptions.delete({ - userId: user.id, - endpoint: ps.endpoint, - }); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.swSubscriptionsRepository) + private swSubscriptionsRepository: SwSubscriptionsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.swSubscriptionsRepository.delete({ + userId: me.id, + endpoint: ps.endpoint, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 9949237a7e..39ea1f2171 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,4 +1,5 @@ -import define from '../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { tags: ['non-productive'], @@ -21,6 +22,12 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - return ps; -}); +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async (ps, me) => { + return ps; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 3e41aeaed8..56474d6988 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,6 +1,9 @@ import { IsNull } from 'typeorm'; -import { Users, UsedUsernames } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { localUsernameSchema } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -22,22 +25,33 @@ export const meta = { export const paramDef = { type: 'object', properties: { - username: Users.localUsernameSchema, + username: localUsernameSchema, }, required: ['username'], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - // Get exist - const exist = await Users.countBy({ - host: IsNull(), - usernameLower: ps.username.toLowerCase(), - }); - - const exist2 = await UsedUsernames.countBy({ username: ps.username.toLowerCase() }); - - return { - available: exist === 0 && exist2 === 0, - }; -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.usedUsernamesRepository) + private usedUsernamesRepository: UsedUsernamesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + // Get exist + const exist = await this.usersRepository.countBy({ + host: IsNull(), + usernameLower: ps.username.toLowerCase(), + }); + + const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); + + return { + available: exist === 0 && exist2 === 0, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 3a8211374b..3d05ec2e1d 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,7 +1,9 @@ -import { Users } from '@/models/index.js'; -import define from '../define.js'; -import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query.js'; -import { generateBlockQueryForUsers } from '../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -38,43 +40,54 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user'); - query.where('user.isExplorable = TRUE'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - 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; - } + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.usersRepository.createQueryBuilder('user'); + query.where('user.isExplorable = TRUE'); - switch (ps.origin) { - case 'local': query.andWhere('user.host IS NULL'); break; - case 'remote': query.andWhere('user.host IS NOT NULL'); break; - } + 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; + } - if (ps.hostname) { - query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); - } + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; - case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; - default: query.orderBy('user.id', 'ASC'); break; - } + if (ps.hostname) { + query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); + } + + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; + default: query.orderBy('user.id', 'ASC'); break; + } - if (me) generateMutedUserQueryForUsers(query, me); - if (me) generateBlockQueryForUsers(query, me); + if (me) this.queryService.generateMutedUserQueryForUsers(query, me); + if (me) this.queryService.generateBlockQueryForUsers(query, me); - query.take(ps.limit); - query.skip(ps.offset); + query.take(ps.limit); + query.skip(ps.offset); - const users = await query.getMany(); + const users = await query.getMany(); - return await Users.packMany(users, me, { detail: true }); -}); + return await this.userEntityService.packMany(users, me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 09fdf27c23..2d5545cbab 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,6 +1,9 @@ -import { Clips } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClipsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users', 'clips'], @@ -30,14 +33,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) - .andWhere('clip.userId = :userId', { userId: ps.userId }) - .andWhere('clip.isPublic = true'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, - const clips = await query - .take(ps.limit) - .getMany(); + private clipEntityService: ClipEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId) + .andWhere('clip.userId = :userId', { userId: ps.userId }) + .andWhere('clip.isPublic = true'); - return await Clips.packMany(clips); -}); + const clips = await query + .take(ps.limit) + .getMany(); + + return await this.clipEntityService.packMany(clips); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 7f9f980764..08bcdd9f88 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,9 +1,12 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['users'], @@ -66,42 +69,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy(ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) ?? IsNull() }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); + private utilityService: UtilityService, + private followingEntityService: FollowingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); } - } - } - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere('following.followeeId = :userId', { userId: user.id }) - .innerJoinAndSelect('following.follower', 'follower'); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const followings = await query - .take(ps.limit) - .getMany(); + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await this.followingsRepository.findOneBy({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { + throw new ApiError(meta.errors.forbidden); + } + } + } + + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere('following.followeeId = :userId', { userId: user.id }) + .innerJoinAndSelect('following.follower', 'follower'); - return await Followings.packMany(followings, me, { populateFollower: true }); -}); + const followings = await query + .take(ps.limit) + .getMany(); + + return await this.followingEntityService.packMany(followings, me, { populateFollower: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 0aaa810f76..225ab5210a 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,9 +1,12 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['users'], @@ -66,42 +69,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy(ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) ?? IsNull() }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); + private utilityService: UtilityService, + private followingEntityService: FollowingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); } - } - } - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere('following.followerId = :userId', { userId: user.id }) - .innerJoinAndSelect('following.followee', 'followee'); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const followings = await query - .take(ps.limit) - .getMany(); + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await this.followingsRepository.findOneBy({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { + throw new ApiError(meta.errors.forbidden); + } + } + } + + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere('following.followerId = :userId', { userId: user.id }) + .innerJoinAndSelect('following.followee', 'followee'); - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); + const followings = await query + .take(ps.limit) + .getMany(); + + return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 35bf2df598..2d28d6ca07 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { GalleryPosts } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users', 'gallery'], @@ -30,13 +33,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere(`post.userId = :userId`, { userId: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const posts = await query - .take(ps.limit) - .getMany(); + private galleryPostEntityService: GalleryPostEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + .andWhere('post.userId = :userId', { userId: ps.userId }); - return await GalleryPosts.packMany(posts, user); -}); + const posts = await query + .take(ps.limit) + .getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 56965d3066..3eeca7562f 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,9 +1,12 @@ import { Not, In, IsNull } from 'typeorm'; -import { maximum } from '@/prelude/array.js'; -import { Notes, Users } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { maximum } from '@/misc/prelude/array.js'; +import { NotesRepository, UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['users'], @@ -51,64 +54,78 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Fetch recent notes - const recentNotes = await Notes.find({ - where: { - userId: user.id, - replyId: Not(IsNull()), - }, - order: { - id: -1, - }, - take: 1000, - select: ['replyId'], - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Fetch recent notes + const recentNotes = await this.notesRepository.find({ + where: { + userId: user.id, + replyId: Not(IsNull()), + }, + order: { + id: -1, + }, + take: 1000, + select: ['replyId'], + }); + + // 投稿が少なかったら中断 + if (recentNotes.length === 0) { + return []; + } + + // TODO ミュートを考慮 + const replyTargetNotes = await this.notesRepository.find({ + where: { + id: In(recentNotes.map(p => p.replyId)), + }, + select: ['userId'], + }); - // 投稿が少なかったら中断 - if (recentNotes.length === 0) { - return []; - } + const repliedUsers: any = {}; - // TODO ミュートを考慮 - const replyTargetNotes = await Notes.find({ - where: { - id: In(recentNotes.map(p => p.replyId)), - }, - select: ['userId'], - }); - - const repliedUsers: any = {}; - - // Extract replies from recent notes - for (const userId of replyTargetNotes.map(x => x.userId.toString())) { - if (repliedUsers[userId]) { - repliedUsers[userId]++; - } else { - repliedUsers[userId] = 1; - } - } + // Extract replies from recent notes + for (const userId of replyTargetNotes.map(x => x.userId.toString())) { + if (repliedUsers[userId]) { + repliedUsers[userId]++; + } else { + repliedUsers[userId] = 1; + } + } - // Calc peak - const peak = maximum(Object.values(repliedUsers)); + // Calc peak + const peak = maximum(Object.values(repliedUsers)); - // Sort replies by frequency - const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); + // Sort replies by frequency + const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); - // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); - // Make replies object (includes weights) - const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await Users.pack(user, me, { detail: true }), - weight: repliedUsers[user] / peak, - }))); + // Make replies object (includes weights) + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await this.userEntityService.pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak, + }))); - return repliesObj; -}); + return repliesObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index 4a6362a3c6..5d7ad84ae0 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,8 +1,11 @@ -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['groups'], @@ -29,21 +32,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const userGroup = await UserGroups.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserGroup).then(x => UserGroups.findOneByOrFail(x.identifiers[0])); - - // Push the owner - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id, - } as UserGroupJoining); - - return await UserGroups.pack(userGroup); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private userGroupEntityService: UserGroupEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const userGroup = await this.userGroupsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + } as UserGroup).then(x => this.userGroupsRepository.findOneByOrFail(x.identifiers[0])); + + // Push the owner + await this.userGroupJoiningsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + userGroupId: userGroup.id, + } as UserGroupJoining); + + return await this.userGroupEntityService.pack(userGroup); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index 2ff1f9aec1..50156b049e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -1,5 +1,7 @@ -import { UserGroups } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -29,15 +31,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: user.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + await this.userGroupsRepository.delete(userGroup.id); + }); } - - await UserGroups.delete(userGroup.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 220fff5f3e..0490fd41a0 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,8 +1,10 @@ -import { UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupInvitationsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../../error.js'; -import define from '../../../../define.js'; export const meta = { tags: ['groups', 'users'], @@ -31,27 +33,40 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the invitation + const invitation = await this.userGroupInvitationsRepository.findOneBy({ + id: ps.invitationId, + }); - // Push the user - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: invitation.userGroupId, - } as UserGroupJoining); + if (invitation == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } - UserGroupInvitations.delete(invitation.id); -}); + if (invitation.userId !== me.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + // Push the user + await this.userGroupJoiningsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + userGroupId: invitation.userGroupId, + } as UserGroupJoining); + + this.userGroupInvitationsRepository.delete(invitation.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 8d1d3db734..26efc1ecf3 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -1,5 +1,7 @@ -import { UserGroupInvitations } from '@/models/index.js'; -import define from '../../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupInvitationsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../../error.js'; export const meta = { @@ -29,19 +31,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the invitation + const invitation = await this.userGroupInvitationsRepository.findOneBy({ + id: ps.invitationId, + }); + + if (invitation == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + if (invitation.userId !== me.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + await this.userGroupInvitationsRepository.delete(invitation.id); + }); } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - await UserGroupInvitations.delete(invitation.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 1a8d320f3a..4ae32a6bda 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -1,10 +1,12 @@ -import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; -import { createNotification } from '@/services/create-notification.js'; -import { getUser } from '../../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvitationsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import define from '../../../define.js'; export const meta = { tags: ['groups', 'users'], @@ -52,51 +54,69 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private idService: IdService, + private getterService: GetterService, + private createNotificationService: CreateNotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userGroupId: userGroup.id, + userId: user.id, + }); + + if (joining) { + throw new ApiError(meta.errors.alreadyAdded); + } + + const existInvitation = await this.userGroupInvitationsRepository.findOneBy({ + userGroupId: userGroup.id, + userId: user.id, + }); + + if (existInvitation) { + throw new ApiError(meta.errors.alreadyInvited); + } + + const invitation = await this.userGroupInvitationsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id, + } as UserGroupInvitation).then(x => this.userGroupInvitationsRepository.findOneByOrFail(x.identifiers[0])); + + // 通知を作成 + this.createNotificationService.createNotification(user.id, 'groupInvited', { + notifierId: me.id, + userGroupInvitationId: invitation.id, + }); + }); } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const joining = await UserGroupJoinings.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining) { - throw new ApiError(meta.errors.alreadyAdded); - } - - const existInvitation = await UserGroupInvitations.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (existInvitation) { - throw new ApiError(meta.errors.alreadyInvited); - } - - const invitation = await UserGroupInvitations.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id, - } as UserGroupInvitation).then(x => UserGroupInvitations.findOneByOrFail(x.identifiers[0])); - - // 通知を作成 - createNotification(user.id, 'groupInvited', { - notifierId: me.id, - userGroupInvitationId: invitation.id, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index 16c6e544e5..e7e69f257d 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -1,6 +1,9 @@ import { Not, In } from 'typeorm'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['groups', 'account'], @@ -29,17 +32,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const ownedGroups = await UserGroups.findBy({ - userId: me.id, - }); - - const joinings = await UserGroupJoinings.findBy({ - userId: me.id, - ...(ownedGroups.length > 0 ? { - userGroupId: Not(In(ownedGroups.map(x => x.id))), - } : {}), - }); - - return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private userGroupEntityService: UserGroupEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const ownedGroups = await this.userGroupsRepository.findBy({ + userId: me.id, + }); + + const joinings = await this.userGroupJoiningsRepository.findBy({ + userId: me.id, + ...(ownedGroups.length > 0 ? { + userGroupId: Not(In(ownedGroups.map(x => x.id))), + } : {}), + }); + + return await Promise.all(joinings.map(x => this.userGroupEntityService.pack(x.userGroupId))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 83dc757db1..0a63dbb7f1 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -1,5 +1,7 @@ -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -35,19 +37,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, - if (me.id === userGroup.userId) { - throw new ApiError(meta.errors.youAreOwner); - } + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } - await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id }); -}); + if (me.id === userGroup.userId) { + throw new ApiError(meta.errors.youAreOwner); + } + + await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: me.id }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index d77cf1a52e..c9ae39561f 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -1,5 +1,8 @@ -import { UserGroups } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['groups', 'account'], @@ -28,10 +31,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const userGroups = await UserGroups.findBy({ - userId: me.id, - }); - - return await Promise.all(userGroups.map(x => UserGroups.pack(x))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + private userGroupEntityService: UserGroupEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const userGroups = await this.userGroupsRepository.findBy({ + userId: me.id, + }); + + return await Promise.all(userGroups.map(x => this.userGroupEntityService.pack(x))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index ba67a1e5c9..e6f60eef0a 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,7 +1,9 @@ -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['groups', 'users'], @@ -43,27 +45,40 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + userId: me.id, + }); - if (user.id === userGroup.userId) { - throw new ApiError(meta.errors.isOwner); - } + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); - // Pull the user - await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id }); -}); + if (user.id === userGroup.userId) { + throw new ApiError(meta.errors.isOwner); + } + + // Pull the user + await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: user.id }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 21e3d9da26..1cebfcd204 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -1,5 +1,8 @@ -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -35,24 +38,37 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, - const joining = await UserGroupJoinings.findOneBy({ - userId: me.id, - userGroupId: userGroup.id, - }); + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, - if (joining == null && userGroup.userId !== me.id) { - throw new ApiError(meta.errors.noSuchGroup); - } + private userGroupEntityService: UserGroupEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } - return await UserGroups.pack(userGroup); -}); + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userId: me.id, + userGroupId: userGroup.id, + }); + + if (joining == null && userGroup.userId !== me.id) { + throw new ApiError(meta.errors.noSuchGroup); + } + + return await this.userGroupEntityService.pack(userGroup); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index 6456e70dd5..a8b2533b73 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -1,7 +1,10 @@ -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['groups', 'users'], @@ -49,35 +52,49 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + private userGroupEntityService: UserGroupEntityService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + const joining = await this.userGroupJoiningsRepository.findOneBy({ + userGroupId: userGroup.id, + userId: user.id, + }); + + if (joining == null) { + throw new ApiError(meta.errors.noSuchGroupMember); + } + + await this.userGroupsRepository.update(userGroup.id, { + userId: ps.userId, + }); + + return await this.userGroupEntityService.pack(userGroup.id); + }); } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const joining = await UserGroupJoinings.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.noSuchGroupMember); - } - - await UserGroups.update(userGroup.id, { - userId: ps.userId, - }); - - return await UserGroups.pack(userGroup.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index 0a96165fc4..b679625c85 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -1,5 +1,8 @@ -import { UserGroups } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -36,20 +39,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userGroupsRepository) + private userGroupsRepository: UserGroupsRepository, + + private userGroupEntityService: UserGroupEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the group + const userGroup = await this.userGroupsRepository.findOneBy({ + id: ps.groupId, + userId: me.id, + }); - await UserGroups.update(userGroup.id, { - name: ps.name, - }); + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } - return await UserGroups.pack(userGroup.id); -}); + await this.userGroupsRepository.update(userGroup.id, { + name: ps.name, + }); + + return await this.userGroupEntityService.pack(userGroup.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 783e63f5de..aa64ca1229 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,7 +1,10 @@ -import { UserLists } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { UserList } from '@/models/entities/user-list.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['lists'], @@ -28,13 +31,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const userList = await UserLists.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserList).then(x => UserLists.findOneByOrFail(x.identifiers[0])); - - return await UserLists.pack(userList); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListEntityService: UserListEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.userListEntityService.pack(userList); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index 5a7613c98a..0f4125a39f 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,5 +1,7 @@ -import { UserLists } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -29,15 +31,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + await this.userListsRepository.delete(userList.id); + }); } - - await UserLists.delete(userList.id); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 889052fa30..919de22377 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,8 @@ -import { UserLists } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['lists', 'account'], @@ -28,10 +31,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const userLists = await UserLists.findBy({ - userId: me.id, - }); - - return await Promise.all(userLists.map(x => UserLists.pack(x))); -}); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListEntityService: UserListEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const userLists = await this.userListsRepository.findBy({ + userId: me.id, + }); + + return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index d3d1d6555c..89d97be93e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,8 +1,11 @@ -import { publishUserListStream } from '@/services/stream.js'; -import { UserLists, UserListJoinings, Users } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['lists', 'users'], @@ -38,25 +41,40 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } - // Pull the user - await UserListJoinings.delete({ userListId: userList.id, userId: user.id }); + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); - publishUserListStream(userList.id, 'userRemoved', await Users.pack(user)); -}); + // Pull the user + await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id }); + + this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user)); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 12b7b86342..77ad772b13 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,8 +1,10 @@ -import { pushUserToUserList } from '@/services/user-list/push.js'; -import { UserLists, UserListJoinings, Blockings } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/common/GetterService.js'; +import { UserListService } from '@/core/UserListService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; export const meta = { tags: ['lists', 'users'], @@ -50,43 +52,60 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check blocking - if (user.id !== me.id) { - const block = await Blockings.findOneBy({ - blockerId: user.id, - blockeeId: me.id, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private getterService: GetterService, + private userListService: UserListService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check blocking + if (user.id !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: user.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: user.id, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + // Push the user + await this.userListService.push(user, userList); }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - const exist = await UserListJoinings.findOneBy({ - userListId: userList.id, - userId: user.id, - }); - - if (exist) { - throw new ApiError(meta.errors.alreadyAdded); } - - // Push the user - await pushUserToUserList(user, userList); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index fd0612f735..62e730b2f7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,8 @@ -import { UserLists } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -35,16 +38,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListEntityService: UserListEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + return await this.userListEntityService.pack(userList); + }); } - - return await UserLists.pack(userList); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 65e708b959..c6669d24d1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,5 +1,8 @@ -import { UserLists } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -36,20 +39,30 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListEntityService: UserListEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); - await UserLists.update(userList.id, { - name: ps.name, - }); + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } - return await UserLists.pack(userList.id); -}); + await this.userListsRepository.update(userList.id, { + name: ps.name, + }); + + return await this.userListEntityService.pack(userList.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 9fa56fe83a..bb8104584c 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,12 +1,12 @@ import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['users', 'notes'], @@ -53,70 +53,82 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - - generateVisibilityQuery(query, me); - if (me) { - generateMutedUserQuery(query, me, user); - generateBlockedUserQuery(query, me); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me, user); + this.queryService.generateBlockedUserQuery(query, me); + } - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } - })); - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); + + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + } + } - if (!ps.includeReplies) { - query.andWhere('note.replyId IS NULL'); - } + if (!ps.includeReplies) { + query.andWhere('note.replyId IS NULL'); + } - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :userId', { userId: user.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - //#endregion + //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.take(ps.limit).getMany(); - return await Notes.packMany(timeline, me); -}); + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index b1d28af845..96c7ef1e70 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,6 +1,9 @@ -import { Pages } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { PagesRepository } from '@/models'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users', 'pages'], @@ -30,14 +33,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere('page.userId = :userId', { userId: ps.userId }) - .andWhere('page.visibility = \'public\''); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - const pages = await query - .take(ps.limit) - .getMany(); + private pageEntityService: PageEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId) + .andWhere('page.userId = :userId', { userId: ps.userId }) + .andWhere('page.visibility = \'public\''); - return await Pages.packMany(pages); -}); + const pages = await query + .take(ps.limit) + .getMany(); + + return await this.pageEntityService.packMany(pages); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 9668bd21b8..6b4d882b7c 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,7 +1,9 @@ -import { NoteReactions, UserProfiles } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserProfilesRepository, NoteReactionsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,23 +46,37 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { - throw new ApiError(meta.errors.reactionsNotPublic); - } + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + private noteReactionEntityService: NoteReactionEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); - const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('reaction.userId = :userId', { userId: ps.userId }) - .leftJoinAndSelect('reaction.note', 'note'); + if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { + throw new ApiError(meta.errors.reactionsNotPublic); + } - generateVisibilityQuery(query, me); + const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('reaction.userId = :userId', { userId: ps.userId }) + .leftJoinAndSelect('reaction.note', 'note'); - const reactions = await query - .take(ps.limit) - .getMany(); + this.queryService.generateVisibilityQuery(query, me); - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true }))); -}); + const reactions = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index e7654e1714..e50a5706d9 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,8 +1,10 @@ import ms from 'ms'; -import { Users, Followings } from '@/models/index.js'; -import define from '../../define.js'; -import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -34,29 +36,43 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user') - .where('user.isLocked = FALSE') - .andWhere('user.isExplorable = TRUE') - .andWhere('user.host IS NULL') - .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) - .andWhere('user.id != :meId', { meId: me.id }) - .orderBy('user.followersCount', 'DESC'); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - generateMutedUserQueryForUsers(query, me); - generateBlockQueryForUsers(query, me); - generateBlockedUserQuery(query, me); + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.usersRepository.createQueryBuilder('user') + .where('user.isLocked = FALSE') + .andWhere('user.isExplorable = TRUE') + .andWhere('user.host IS NULL') + .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .andWhere('user.id != :meId', { meId: me.id }) + .orderBy('user.followersCount', 'DESC'); - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); + this.queryService.generateMutedUserQueryForUsers(query, me); + this.queryService.generateBlockQueryForUsers(query, me); + this.queryService.generateBlockedUserQuery(query, me); - query - .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - query.setParameters(followingQuery.getParameters()); + query + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); - const users = await query.take(ps.limit).skip(ps.offset).getMany(); + query.setParameters(followingQuery.getParameters()); - return await Users.packMany(users, me, { detail: true }); -}); + const users = await query.take(ps.limit).skip(ps.offset).getMany(); + + return await this.userEntityService.packMany(users, me, { detail: true }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 233a6a90b4..aea75ae799 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,5 +1,8 @@ -import { Users } from '@/models/index.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -112,10 +115,20 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id))); + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - return Array.isArray(ps.userId) ? relations : relations[0]; -}); + const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); + + return Array.isArray(ps.userId) ? relations : relations[0]; + }); + } +} 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 a9987eafa9..5c211a9017 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,12 +1,14 @@ import * as sanitizeHtml from 'sanitize-html'; -import { publishAdminStream } from '@/services/stream.js'; -import { AbuseUserReports, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { sendEmail } from '@/services/send-email.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { getUser } from '../../common/getters.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +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 define from '../../define.js'; +import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['users'], @@ -46,55 +48,72 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - if (user.id === me.id) { - throw new ApiError(meta.errors.cannotReportYourself); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (user.isAdmin) { - throw new ApiError(meta.errors.cannotReportAdmin); - } + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, - const report = await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: user.id, - targetUserHost: user.host, - reporterId: me.id, - reporterHost: null, - comment: ps.comment, - }).then(x => AbuseUserReports.findOneByOrFail(x.identifiers[0])); - - // Publish event to moderators - setImmediate(async () => { - const moderators = await Users.find({ - where: [{ - isAdmin: true, - }, { - isModerator: true, - }], - }); + private idService: IdService, + private metaService: MetaService, + private emailService: EmailService, + private getterService: GetterService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Lookup user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + if (user.id === me.id) { + throw new ApiError(meta.errors.cannotReportYourself); + } + + if (user.isAdmin) { + throw new ApiError(meta.errors.cannotReportAdmin); + } - for (const moderator of moderators) { - publishAdminStream(moderator.id, 'newAbuseUserReport', { - id: report.id, - targetUserId: report.targetUserId, - reporterId: report.reporterId, - comment: report.comment, + const report = await this.abuseUserReportsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + targetUserId: user.id, + targetUserHost: user.host, + reporterId: me.id, + reporterHost: null, + comment: ps.comment, + }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish event to moderators + setImmediate(async () => { + const moderators = await this.usersRepository.find({ + where: [{ + isAdmin: true, + }, { + isModerator: true, + }], + }); + + for (const moderator of moderators) { + this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, + targetUserId: report.targetUserId, + reporterId: report.reporterId, + comment: report.comment, + }); + } + + const meta = await this.metaService.fetch(); + if (meta.email) { + this.emailService.sendEmail(meta.email, 'New abuse report', + sanitizeHtml(ps.comment), + sanitizeHtml(ps.comment)); + } }); - } - - const meta = await fetchMeta(); - if (meta.email) { - sendEmail(meta.email, 'New abuse report', - sanitizeHtml(ps.comment), - sanitizeHtml(ps.comment)); - } - }); -}); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 6e5bc46bb5..1747dc93f6 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,8 +1,11 @@ import { Brackets } from 'typeorm'; -import { Followings, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { USER_ACTIVE_THRESHOLD } from '@/const.js'; -import { User } from '@/models/entities/user.js'; -import define from '../../define.js'; +import type { User } from '@/models/entities/User.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -39,78 +42,91 @@ export const paramDef = { // TODO: avatar,bannerをJOINしたいけどエラーになる // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - if (ps.host) { - const q = Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); - - if (ps.username) { - q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); - } - - q.andWhere('user.updatedAt IS NOT NULL'); - q.orderBy('user.updatedAt', 'DESC'); - - const users = await q.take(ps.limit).getMany(); - - return await Users.packMany(users, me, { detail: ps.detail }); - } else if (ps.username) { - let users: User[] = []; - - if (me) { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const query = Users.createQueryBuilder('user') - .where(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })); - - query.setParameters(followingQuery.getParameters()); - - users = await query - .orderBy('user.usernameLower', 'ASC') - .take(ps.limit) - .getMany(); - - if (users.length < ps.limit) { - const otherQuery = await Users.createQueryBuilder('user') - .where(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere('user.updatedAt IS NOT NULL'); - - otherQuery.setParameters(followingQuery.getParameters()); - - const otherUsers = await otherQuery - .orderBy('user.updatedAt', 'DESC') - .take(ps.limit - users.length) - .getMany(); - - users = users.concat(otherUsers); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + if (ps.host) { + const q = this.usersRepository.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); + + if (ps.username) { + q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); + } + + q.andWhere('user.updatedAt IS NOT NULL'); + q.orderBy('user.updatedAt', 'DESC'); + + const users = await q.take(ps.limit).getMany(); + + return await this.userEntityService.packMany(users, me, { detail: ps.detail }); + } else if (ps.username) { + let users: User[] = []; + + if (me) { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere('user.id != :meId', { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .take(ps.limit) + .getMany(); + + if (users.length < ps.limit) { + const otherQuery = await this.usersRepository.createQueryBuilder('user') + .where(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere('user.id != :meId', { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + } else { + users = await this.usersRepository.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit - users.length) + .getMany(); + } + + return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); } - } else { - users = await Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere('user.updatedAt IS NOT NULL') - .orderBy('user.updatedAt', 'DESC') - .take(ps.limit - users.length) - .getMany(); - } - - return await Users.packMany(users, me, { detail: !!ps.detail }); - } - return []; -}); + return []; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 01729de667..9879b1b68b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,7 +1,10 @@ import { Brackets } from 'typeorm'; -import { UserProfiles, Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -34,89 +37,102 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - const isUsername = ps.query.startsWith('@'); - - let users: User[] = []; - - if (isUsername) { - const usernameQuery = Users.createQueryBuilder('user') - .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - usernameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - usernameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await usernameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) - .getMany(); - } else { - const nameQuery = Users.createQueryBuilder('user') - .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); - - // Also search username if it qualifies as username - if (Users.validateLocalUsername(ps.query)) { - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + const isUsername = ps.query.startsWith('@'); + + let users: User[] = []; + + if (isUsername) { + const usernameQuery = this.usersRepository.createQueryBuilder('user') + .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + usernameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + usernameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await usernameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + } else { + const nameQuery = this.usersRepository.createQueryBuilder('user') + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + + // Also search username if it qualifies as username + if (this.userEntityService.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + } + })) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await nameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit) { + const profQuery = this.userProfilesRepository.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); + + if (ps.origin === 'local') { + profQuery.andWhere('prof.userHost IS NULL'); + } else if (ps.origin === 'remote') { + profQuery.andWhere('prof.userHost IS NOT NULL'); + } + + const query = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); + + users = users.concat(await query + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(), + ); } - })) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await nameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) - .getMany(); - - if (users.length < ps.limit) { - const profQuery = UserProfiles.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); - - if (ps.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); - } else if (ps.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); } - const query = Users.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); - - users = users.concat(await query - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) - .getMany(), - ); - } + return await this.userEntityService.packMany(users, me, { detail: ps.detail }); + }); } - - return await Users.packMany(users, me, { detail: ps.detail }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 846d83b49f..98f5f03063 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,10 +1,14 @@ -import { FindOptionsWhere, In, IsNull } from 'typeorm'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import define from '../../define.js'; -import { apiLogger } from '../../logger.js'; +import { In, IsNull } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { ApiLoggerService } from '../../ApiLoggerService.js'; +import type { FindOptionsWhere } from 'typeorm'; export const meta = { tags: ['users'], @@ -78,53 +82,65 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - let user; - - const isAdminOrModerator = me && (me.isAdmin || me.isModerator); - - if (ps.userIds) { - if (ps.userIds.length === 0) { - return []; - } - - const users = await Users.findBy(isAdminOrModerator ? { - id: In(ps.userIds), - } : { - id: In(ps.userIds), - isSuspended: false, - }); - - // リクエストされた通りに並べ替え - const _users: User[] = []; - for (const id of ps.userIds) { - _users.push(users.find(x => x.id === id)!); - } - - return await Promise.all(_users.map(u => Users.pack(u, me, { - detail: true, - }))); - } else { - // Lookup user - if (typeof ps.host === 'string' && typeof ps.username === 'string') { - user = await resolveUser(ps.username, ps.host).catch(e => { - apiLogger.warn(`failed to resolve remote user: ${e}`); - throw new ApiError(meta.errors.failedToResolveRemoteUser); - }); - } else { - const q: FindOptionsWhere = ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; - - user = await Users.findOneBy(q); - } - - if (user == null || (!isAdminOrModerator && user.isSuspended)) { - throw new ApiError(meta.errors.noSuchUser); - } - - return await Users.pack(user, me, { - detail: true, +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + private resolveUserService: ResolveUserService, + private apiLoggerService: ApiLoggerService, + ) { + super(meta, paramDef, async (ps, me) => { + let user; + + const isAdminOrModerator = me && (me.isAdmin || me.isModerator); + + if (ps.userIds) { + if (ps.userIds.length === 0) { + return []; + } + + const users = await this.usersRepository.findBy(isAdminOrModerator ? { + id: In(ps.userIds), + } : { + id: In(ps.userIds), + isSuspended: false, + }); + + // リクエストされた通りに並べ替え + const _users: User[] = []; + for (const id of ps.userIds) { + _users.push(users.find(x => x.id === id)!); + } + + return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { + detail: true, + }))); + } else { + // Lookup user + if (typeof ps.host === 'string' && typeof ps.username === 'string') { + user = await this.resolveUserService.resolveUser(ps.username, ps.host).catch(err => { + this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`); + throw new ApiError(meta.errors.failedToResolveRemoteUser); + }); + } else { + const q: FindOptionsWhere = ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; + + user = await this.usersRepository.findOneBy(q); + } + + if (user == null || (!isAdminOrModerator && user.isSuspended)) { + throw new ApiError(meta.errors.noSuchUser); + } + + return await this.userEntityService.pack(user, me, { + detail: true, + }); + } }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index 47f322ee9b..71f4ca0cfa 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,6 +1,8 @@ -import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -116,78 +118,109 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, - const result = await awaitAll({ - notesCount: Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - repliesCount: Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.replyId IS NOT NULL') - .getCount(), - renotesCount: Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.renoteId IS NOT NULL') - .getCount(), - repliedCount: Notes.createQueryBuilder('note') - .where('note.replyUserId = :userId', { userId: user.id }) - .getCount(), - renotedCount: Notes.createQueryBuilder('note') - .where('note.renoteUserId = :userId', { userId: user.id }) - .getCount(), - pollVotesCount: PollVotes.createQueryBuilder('vote') - .where('vote.userId = :userId', { userId: user.id }) - .getCount(), - pollVotedCount: PollVotes.createQueryBuilder('vote') - .innerJoin('vote.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - localFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - remoteFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - localFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - remoteFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), - sentReactionsCount: NoteReactions.createQueryBuilder('reaction') - .where('reaction.userId = :userId', { userId: user.id }) - .getCount(), - receivedReactionsCount: NoteReactions.createQueryBuilder('reaction') - .innerJoin('reaction.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), - pageLikesCount: PageLikes.createQueryBuilder('like') - .where('like.userId = :userId', { userId: user.id }) - .getCount(), - pageLikedCount: PageLikes.createQueryBuilder('like') - .innerJoin('like.page', 'page') - .where('page.userId = :userId', { userId: user.id }) - .getCount(), - driveFilesCount: DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .getCount(), - driveUsage: DriveFiles.calcDriveUsageOf(user), - }); - - result.followingCount = result.localFollowingCount + result.remoteFollowingCount; - result.followersCount = result.localFollowersCount + result.remoteFollowersCount; - - return result; -}); + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const result = await awaitAll({ + notesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + repliesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.replyId IS NOT NULL') + .getCount(), + renotesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.renoteId IS NOT NULL') + .getCount(), + repliedCount: this.notesRepository.createQueryBuilder('note') + .where('note.replyUserId = :userId', { userId: user.id }) + .getCount(), + renotedCount: this.notesRepository.createQueryBuilder('note') + .where('note.renoteUserId = :userId', { userId: user.id }) + .getCount(), + pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') + .where('vote.userId = :userId', { userId: user.id }) + .getCount(), + pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') + .innerJoin('vote.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + localFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + localFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .where('reaction.userId = :userId', { userId: user.id }) + .getCount(), + receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .innerJoin('reaction.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') + .where('favorite.userId = :userId', { userId: user.id }) + .getCount(), + pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') + .where('like.userId = :userId', { userId: user.id }) + .getCount(), + pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') + .innerJoin('like.page', 'page') + .where('page.userId = :userId', { userId: user.id }) + .getCount(), + driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }) + .getCount(), + driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), + }); + + result.followingCount = result.localFollowingCount + result.remoteFollowingCount; + result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + + return result; + }); + } +} diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 3f0861fdb1..347d5650ad 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -8,8 +8,8 @@ export class ApiError extends Error { public httpStatusCode?: number; public info?: any; - constructor(e?: E | null | undefined, info?: any | null | undefined) { - if (e == null) e = { + constructor(err?: E | null | undefined, info?: any | null | undefined) { + if (err == null) err = { message: 'Internal error occurred. Please contact us if the error persists.', code: 'INTERNAL_ERROR', id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', @@ -17,12 +17,12 @@ export class ApiError extends Error { httpStatusCode: 500, }; - super(e.message); - this.message = e.message; - this.code = e.code; - this.id = e.id; - this.kind = e.kind || 'client'; - this.httpStatusCode = e.httpStatusCode; + super(err.message); + this.message = err.message; + this.code = err.code; + this.id = err.id; + this.kind = err.kind ?? 'client'; + this.httpStatusCode = err.httpStatusCode; this.info = info; } } diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts deleted file mode 100644 index 83ece51f51..0000000000 --- a/packages/backend/src/server/api/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * API Server - */ - -import Koa from 'koa'; -import Router from '@koa/router'; -import multer from '@koa/multer'; -import bodyParser from 'koa-bodyparser'; -import cors from '@koa/cors'; - -import { Instances, AccessTokens, Users } from '@/models/index.js'; -import config from '@/config/index.js'; -import endpoints from './endpoints.js'; -import handler from './api-handler.js'; -import signup from './private/signup.js'; -import signin from './private/signin.js'; -import signupPending from './private/signup-pending.js'; -import discord from './service/discord.js'; -import github from './service/github.js'; -import twitter from './service/twitter.js'; - -// Init app -const app = new Koa(); - -app.use(cors({ - origin: '*', -})); - -// No caching -app.use(async (ctx, next) => { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - await next(); -}); - -app.use(bodyParser({ - // リクエストが multipart/form-data でない限りはJSONだと見なす - detectJSON: ctx => !ctx.is('multipart/form-data'), -})); - -// Init multer instance -const upload = multer({ - storage: multer.diskStorage({}), - limits: { - fileSize: config.maxFileSize || 262144000, - files: 1, - }, -}); - -// Init router -const router = new Router(); - -/** - * Register endpoint handlers - */ -for (const endpoint of endpoints) { - if (endpoint.meta.requireFile) { - router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)); - } else { - // 後方互換性のため - if (endpoint.name.includes('-')) { - router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); - - if (endpoint.meta.allowGet) { - router.get(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); - } else { - router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; }); - } - } - - router.post(`/${endpoint.name}`, handler.bind(null, endpoint)); - - if (endpoint.meta.allowGet) { - router.get(`/${endpoint.name}`, handler.bind(null, endpoint)); - } else { - router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; }); - } - } -} - -router.post('/signup', signup); -router.post('/signin', signin); -router.post('/signup-pending', signupPending); - -router.use(discord.routes()); -router.use(github.routes()); -router.use(twitter.routes()); - -router.get('/v1/instance/peers', async ctx => { - const instances = await Instances.find({ - select: ['host'], - }); - - ctx.body = instances.map(instance => instance.host); -}); - -router.post('/miauth/:session/check', async ctx => { - const token = await AccessTokens.findOneBy({ - session: ctx.params.session, - }); - - if (token && token.session != null && !token.fetched) { - AccessTokens.update(token.id, { - fetched: true, - }); - - ctx.body = { - ok: true, - token: token.token, - user: await Users.pack(token.userId, null, { detail: true }), - }; - } else { - ctx.body = { - ok: false, - }; - } -}); - -// Return 404 for unknown API -router.all('(.*)', async ctx => { - ctx.status = 404; -}); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/api/integration/DiscordServerService.ts b/packages/backend/src/server/api/integration/DiscordServerService.ts new file mode 100644 index 0000000000..604013c454 --- /dev/null +++ b/packages/backend/src/server/api/integration/DiscordServerService.ts @@ -0,0 +1,315 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import Router from '@koa/router'; +import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { Config } from '@/config.js'; +import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { SigninService } from '../SigninService.js'; +import type Koa from 'koa'; + +@Injectable() +export class DiscordServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redisClient: Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private httpRequestService: HttpRequestService, + private globalEventService: GlobalEventService, + private metaService: MetaService, + private signinService: SigninService, + ) { + } + + public create() { + const router = new Router(); + + router.get('/disconnect/discord', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + delete profile.integrations.discord; + + await this.userProfilesRepository.update(user.id, { + integrations: profile.integrations, + }); + + ctx.body = 'Discordの連携を解除しました :v:'; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + }); + + const getOAuth2 = async () => { + const meta = await this.metaService.fetch(true); + + if (meta.enableDiscordIntegration) { + return new OAuth2( + meta.discordClientId!, + meta.discordClientSecret!, + 'https://discord.com/', + 'api/oauth2/authorize', + 'api/oauth2/token'); + } else { + return null; + } + }; + + router.get('/connect/discord', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const params = { + redirect_uri: `${this.config.url}/api/dc/cb`, + scope: ['identify'], + state: uuid(), + response_type: 'code', + }; + + this.redisClient.set(userToken, JSON.stringify(params)); + + const oauth2 = await getOAuth2(); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); + }); + + router.get('/signin/discord', async ctx => { + const sessid = uuid(); + + const params = { + redirect_uri: `${this.config.url}/api/dc/cb`, + scope: ['identify'], + state: uuid(), + response_type: 'code', + }; + + ctx.cookies.set('signin_with_discord_sid', sessid, { + path: '/', + secure: this.config.url.startsWith('https'), + httpOnly: true, + }); + + this.redisClient.set(sessid, JSON.stringify(params)); + + const oauth2 = await getOAuth2(); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); + }); + + router.get('/dc/cb', async ctx => { + const userToken = this.#getUserToken(ctx); + + const oauth2 = await getOAuth2(); + + if (!userToken) { + const sessid = ctx.cookies.get('signin_with_discord_sid'); + + if (!sessid) { + ctx.throw(400, 'invalid session'); + return; + } + + const code = ctx.query.code; + + if (!code || typeof code !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise((res, rej) => { + this.redisClient.get(sessid, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => + oauth2!.getOAuthAccessToken(code, { + grant_type: 'authorization_code', + redirect_uri, + }, (err, accessToken, refreshToken, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ + accessToken, + refreshToken, + expiresDate: Date.now() + Number(result.expires_in) * 1000, + }); + } + })); + + const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + 'Authorization': `Bearer ${accessToken}`, + })) as Record; + + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const profile = await this.userProfilesRepository.createQueryBuilder() + .where('"integrations"->\'discord\'->>\'id\' = :id', { id: id }) + .andWhere('"userHost" IS NULL') + .getOne(); + + if (profile == null) { + ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + await this.userProfilesRepository.update(profile.userId, { + integrations: { + ...profile.integrations, + discord: { + id: id, + accessToken: accessToken, + refreshToken: refreshToken, + expiresDate: expiresDate, + username: username, + discriminator: discriminator, + }, + }, + }); + + this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true); + } else { + const code = ctx.query.code; + + if (!code || typeof code !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise((res, rej) => { + this.redisClient.get(userToken, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => + oauth2!.getOAuthAccessToken(code, { + grant_type: 'authorization_code', + redirect_uri, + }, (err, accessToken, refreshToken, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ + accessToken, + refreshToken, + expiresDate: Date.now() + Number(result.expires_in) * 1000, + }); + } + })); + + const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + 'Authorization': `Bearer ${accessToken}`, + })) as Record; + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + await this.userProfilesRepository.update(user.id, { + integrations: { + ...profile.integrations, + discord: { + accessToken: accessToken, + refreshToken: refreshToken, + expiresDate: expiresDate, + id: id, + username: username, + discriminator: discriminator, + }, + }, + }); + + ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + } + }); + + return router; + } + + #getUserToken(ctx: Koa.BaseContext): string | null { + return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; + } + + #compareOrigin(ctx: Koa.BaseContext): boolean { + function normalizeUrl(url?: string): string { + return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; + } + + const referer = ctx.headers['referer']; + + return (normalizeUrl(referer) === normalizeUrl(this.config.url)); + } +} diff --git a/packages/backend/src/server/api/integration/GithubServerService.ts b/packages/backend/src/server/api/integration/GithubServerService.ts new file mode 100644 index 0000000000..6864f21523 --- /dev/null +++ b/packages/backend/src/server/api/integration/GithubServerService.ts @@ -0,0 +1,287 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import Router from '@koa/router'; +import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { Config } from '@/config.js'; +import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { SigninService } from '../SigninService.js'; +import type Koa from 'koa'; + +@Injectable() +export class GithubServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redisClient: Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private httpRequestService: HttpRequestService, + private globalEventService: GlobalEventService, + private metaService: MetaService, + private signinService: SigninService, + ) { + } + + public create() { + const router = new Router(); + + router.get('/disconnect/github', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + delete profile.integrations.github; + + await this.userProfilesRepository.update(user.id, { + integrations: profile.integrations, + }); + + ctx.body = 'GitHubの連携を解除しました :v:'; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + }); + + const getOath2 = async () => { + const meta = await this.metaService.fetch(true); + + if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) { + return new OAuth2( + meta.githubClientId, + meta.githubClientSecret, + 'https://github.com/', + 'login/oauth/authorize', + 'login/oauth/access_token'); + } else { + return null; + } + }; + + router.get('/connect/github', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const params = { + redirect_uri: `${this.config.url}/api/gh/cb`, + scope: ['read:user'], + state: uuid(), + }; + + this.redisClient.set(userToken, JSON.stringify(params)); + + const oauth2 = await getOath2(); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); + }); + + router.get('/signin/github', async ctx => { + const sessid = uuid(); + + const params = { + redirect_uri: `${this.config.url}/api/gh/cb`, + scope: ['read:user'], + state: uuid(), + }; + + ctx.cookies.set('signin_with_github_sid', sessid, { + path: '/', + secure: this.config.url.startsWith('https'), + httpOnly: true, + }); + + this.redisClient.set(sessid, JSON.stringify(params)); + + const oauth2 = await getOath2(); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); + }); + + router.get('/gh/cb', async ctx => { + const userToken = this.#getUserToken(ctx); + + const oauth2 = await getOath2(); + + if (!userToken) { + const sessid = ctx.cookies.get('signin_with_github_sid'); + + if (!sessid) { + ctx.throw(400, 'invalid session'); + return; + } + + const code = ctx.query.code; + + if (!code || typeof code !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise((res, rej) => { + this.redisClient.get(sessid, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken } = await new Promise((res, rej) => + oauth2!.getOAuthAccessToken(code, { + redirect_uri, + }, (err, accessToken, refresh, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ accessToken }); + } + })); + + const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + 'Authorization': `bearer ${accessToken}`, + })) as Record; + if (typeof login !== 'string' || typeof id !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const link = await this.userProfilesRepository.createQueryBuilder() + .where('"integrations"->\'github\'->>\'id\' = :id', { id: id }) + .andWhere('"userHost" IS NULL') + .getOne(); + + if (link == null) { + ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true); + } else { + const code = ctx.query.code; + + if (!code || typeof code !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise((res, rej) => { + this.redisClient.get(userToken, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken } = await new Promise((res, rej) => + oauth2!.getOAuthAccessToken( + code, + { redirect_uri }, + (err, accessToken, refresh, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ accessToken }); + } + })); + + const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + 'Authorization': `bearer ${accessToken}`, + })) as Record; + + if (typeof login !== 'string' || typeof id !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + await this.userProfilesRepository.update(user.id, { + integrations: { + ...profile.integrations, + github: { + accessToken: accessToken, + id: id, + login: login, + }, + }, + }); + + ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + } + }); + + return router; + } + + #getUserToken(ctx: Koa.BaseContext): string | null { + return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; + } + + #compareOrigin(ctx: Koa.BaseContext): boolean { + function normalizeUrl(url?: string): string { + return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; + } + + const referer = ctx.headers['referer']; + + return (normalizeUrl(referer) === normalizeUrl(this.config.url)); + } +} diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts new file mode 100644 index 0000000000..14b5f40ea5 --- /dev/null +++ b/packages/backend/src/server/api/integration/TwitterServerService.ts @@ -0,0 +1,230 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import Router from '@koa/router'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import autwh from 'autwh'; +import { Config } from '@/config.js'; +import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { ILocalUser } from '@/models/entities/User.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { SigninService } from '../SigninService.js'; +import type Koa from 'koa'; + +@Injectable() +export class TwitterServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redisClient: Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private httpRequestService: HttpRequestService, + private globalEventService: GlobalEventService, + private metaService: MetaService, + private signinService: SigninService, + ) { + } + + public create() { + const router = new Router(); + + router.get('/disconnect/twitter', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + delete profile.integrations.twitter; + + await this.userProfilesRepository.update(user.id, { + integrations: profile.integrations, + }); + + ctx.body = 'Twitterの連携を解除しました :v:'; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + }); + + const getTwAuth = async () => { + const meta = await this.metaService.fetch(true); + + if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) { + return autwh({ + consumerKey: meta.twitterConsumerKey, + consumerSecret: meta.twitterConsumerSecret, + callbackUrl: `${this.config.url}/api/tw/cb`, + }); + } else { + return null; + } + }; + + router.get('/connect/twitter', async ctx => { + if (!this.#compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = this.#getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } + + const twAuth = await getTwAuth(); + const twCtx = await twAuth!.begin(); + this.redisClient.set(userToken, JSON.stringify(twCtx)); + ctx.redirect(twCtx.url); + }); + + router.get('/signin/twitter', async ctx => { + const twAuth = await getTwAuth(); + const twCtx = await twAuth!.begin(); + + const sessid = uuid(); + + this.redisClient.set(sessid, JSON.stringify(twCtx)); + + ctx.cookies.set('signin_with_twitter_sid', sessid, { + path: '/', + secure: this.config.url.startsWith('https'), + httpOnly: true, + }); + + ctx.redirect(twCtx.url); + }); + + router.get('/tw/cb', async ctx => { + const userToken = this.#getUserToken(ctx); + + const twAuth = await getTwAuth(); + + if (userToken == null) { + const sessid = ctx.cookies.get('signin_with_twitter_sid'); + + if (sessid == null) { + ctx.throw(400, 'invalid session'); + return; + } + + const get = new Promise((res, rej) => { + this.redisClient.get(sessid, async (_, twCtx) => { + res(twCtx); + }); + }); + + const twCtx = await get; + + const verifier = ctx.query.oauth_verifier; + if (!verifier || typeof verifier !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const result = await twAuth!.done(JSON.parse(twCtx), verifier); + + const link = await this.userProfilesRepository.createQueryBuilder() + .where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId }) + .andWhere('"userHost" IS NULL') + .getOne(); + + if (link == null) { + ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true); + } else { + const verifier = ctx.query.oauth_verifier; + + if (!verifier || typeof verifier !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const get = new Promise((res, rej) => { + this.redisClient.get(userToken, async (_, twCtx) => { + res(twCtx); + }); + }); + + const twCtx = await get; + + const result = await twAuth!.done(JSON.parse(twCtx), verifier); + + const user = await this.usersRepository.findOneByOrFail({ + host: IsNull(), + token: userToken, + }); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + await this.userProfilesRepository.update(user.id, { + integrations: { + ...profile.integrations, + twitter: { + accessToken: result.accessToken, + accessTokenSecret: result.accessTokenSecret, + userId: result.userId, + screenName: result.screenName, + }, + }, + }); + + ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { + detail: true, + includeSecrets: true, + })); + } + }); + + return router; + } + + #getUserToken(ctx: Koa.BaseContext): string | null { + return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; + } + + #compareOrigin(ctx: Koa.BaseContext): boolean { + function normalizeUrl(url?: string): string { + return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; + } + + const referer = ctx.headers['referer']; + + return (normalizeUrl(referer) === normalizeUrl(this.config.url)); + } +} diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts deleted file mode 100644 index 9a7751716e..0000000000 --- a/packages/backend/src/server/api/limiter.ts +++ /dev/null @@ -1,77 +0,0 @@ -import Limiter from 'ratelimiter'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import Logger from '@/services/logger.js'; -import { redisClient } from '../../db/redis.js'; -import { IEndpointMeta } from './endpoints.js'; - -const logger = new Logger('limiter'); - -export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) => new Promise((ok, reject) => { - if (process.env.NODE_ENV === 'test') ok(); - - const hasShortTermLimit = typeof limitation.minInterval === 'number'; - - const hasLongTermLimit = - typeof limitation.duration === 'number' && - typeof limitation.max === 'number'; - - if (hasShortTermLimit) { - min(); - } else if (hasLongTermLimit) { - max(); - } else { - ok(); - } - - // Short-term limit - function min(): void { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval, - max: 1, - db: redisClient, - }); - - minIntervalLimiter.get((err, info) => { - if (err) { - return reject('ERR'); - } - - logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - reject('BRIEF_REQUEST_INTERVAL'); - } else { - if (hasLongTermLimit) { - max(); - } else { - ok(); - } - } - }); - } - - // Long term limit - function max(): void { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration, - max: limitation.max, - db: redisClient, - }); - - limiter.get((err, info) => { - if (err) { - return reject('ERR'); - } - - logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - reject('RATE_LIMIT_EXCEEDED'); - } else { - ok(); - } - }); - } -}); diff --git a/packages/backend/src/server/api/logger.ts b/packages/backend/src/server/api/logger.ts deleted file mode 100644 index ec22d6c3e2..0000000000 --- a/packages/backend/src/server/api/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger.js'; - -export const apiLogger = new Logger('api'); diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts deleted file mode 100644 index 68fa814041..0000000000 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import endpoints from '../endpoints.js'; -import config from '@/config/index.js'; -import { errors as basicErrors } from './errors.js'; -import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; - -export function genOpenapiSpec() { - const spec = { - openapi: '3.0.0', - - info: { - version: 'v1', - title: 'Misskey API', - 'x-logo': { url: '/static-assets/api-doc.png' }, - }, - - externalDocs: { - description: 'Repository', - url: 'https://github.com/misskey-dev/misskey', - }, - - servers: [{ - url: config.apiUrl, - }], - - paths: {} as any, - - components: { - schemas: schemas, - - securitySchemes: { - ApiKeyAuth: { - type: 'apiKey', - in: 'body', - name: 'i', - }, - }, - }, - }; - - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - const errors = {} as any; - - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { - errors[e.code] = { - value: { - error: e, - }, - }; - } - } - - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; - - let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; - desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; - if (endpoint.meta.kind) { - const kind = endpoint.meta.kind; - desc += ` / **Permission**: *${kind}*`; - } - - const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; - const schema = endpoint.params; - - if (endpoint.meta.requireFile) { - schema.properties.file = { - type: 'string', - format: 'binary', - description: 'The file contents.', - }; - schema.required.push('file'); - } - - const info = { - operationId: endpoint.name, - summary: endpoint.name, - description: desc, - externalDocs: { - description: 'Source code', - url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, - }, - ...(endpoint.meta.tags ? { - tags: [endpoint.meta.tags[0]], - } : {}), - ...(endpoint.meta.requireCredential ? { - security: [{ - ApiKeyAuth: [], - }], - } : {}), - requestBody: { - required: true, - content: { - [requestType]: { - schema, - }, - }, - }, - responses: { - ...(endpoint.meta.res ? { - '200': { - description: 'OK (with results)', - content: { - 'application/json': { - schema: resSchema, - }, - }, - }, - } : { - '204': { - description: 'OK (without any results)', - }, - }), - '400': { - description: 'Client error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: { ...errors, ...basicErrors['400'] }, - }, - }, - }, - '401': { - description: 'Authentication error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['401'], - }, - }, - }, - '403': { - description: 'Forbidden error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['403'], - }, - }, - }, - '418': { - description: 'I\'m Ai', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['418'], - }, - }, - }, - ...(endpoint.meta.limit ? { - '429': { - description: 'To many requests', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['429'], - }, - }, - }, - } : {}), - '500': { - description: 'Internal server error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['500'], - }, - }, - }, - }, - }; - - spec.paths['/' + endpoint.name] = { - post: info, - }; - } - - return spec; -} diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts deleted file mode 100644 index 79b31764fd..0000000000 --- a/packages/backend/src/server/api/private/signin.ts +++ /dev/null @@ -1,250 +0,0 @@ -import Koa from 'koa'; -import bcrypt from 'bcryptjs'; -import * as speakeasy from 'speakeasy'; -import signin from '../common/signin.js'; -import config from '@/config/index.js'; -import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { genId } from '@/misc/gen-id.js'; -import { verifyLogin, hash } from '../2fa.js'; -import { randomBytes } from 'node:crypto'; -import { IsNull } from 'typeorm'; -import { limiter } from '../limiter.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; - -export default async (ctx: Koa.Context) => { - ctx.set('Access-Control-Allow-Origin', config.url); - ctx.set('Access-Control-Allow-Credentials', 'true'); - - const body = ctx.request.body as any; - const username = body['username']; - const password = body['password']; - const token = body['token']; - - function error(status: number, error: { id: string }) { - ctx.status = status; - ctx.body = { error }; - } - - try { - // not more than 1 attempt per second and not more than 10 attempts per hour - await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); - } catch (err) { - ctx.status = 429; - ctx.body = { - error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', - id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, - }; - return; - } - - if (typeof username !== 'string') { - ctx.status = 400; - return; - } - - if (typeof password !== 'string') { - ctx.status = 400; - return; - } - - if (token != null && typeof token !== 'string') { - ctx.status = 400; - return; - } - - // Fetch user - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: IsNull(), - }) as ILocalUser; - - if (user == null) { - error(404, { - id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', - }); - return; - } - - if (user.isSuspended) { - error(403, { - id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', - }); - return; - } - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await bcrypt.compare(password, profile.password!); - - async function fail(status?: number, failure?: { id: string }) { - // Append signin history - await Signins.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: false, - }); - - error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); - } - - if (!profile.twoFactorEnabled) { - if (same) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - return; - } - } - - if (token) { - if (!same) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - return; - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: 'base32', - token: token, - window: 2, - }); - - if (verified) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', - }); - return; - } - } else if (body.credentialId) { - if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - return; - } - - const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); - const clientData = JSON.parse(clientDataJSON.toString('utf-8')); - const challenge = await AttestationChallenges.findOneBy({ - userId: user.id, - id: body.challengeId, - registrationChallenge: false, - challenge: hash(clientData.challenge).toString('hex'), - }); - - if (!challenge) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); - return; - } - - await AttestationChallenges.delete({ - userId: user.id, - id: body.challengeId, - }); - - if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); - return; - } - - const securityKey = await UserSecurityKeys.findOneBy({ - id: Buffer.from( - body.credentialId - .replace(/-/g, '+') - .replace(/_/g, '/'), - 'base64' - ).toString('hex'), - }); - - if (!securityKey) { - await fail(403, { - id: '66269679-aeaf-4474-862b-eb761197e046', - }); - return; - } - - const isValid = verifyLogin({ - publicKey: Buffer.from(securityKey.publicKey, 'hex'), - authenticatorData: Buffer.from(body.authenticatorData, 'hex'), - clientDataJSON, - clientData, - signature: Buffer.from(body.signature, 'hex'), - challenge: challenge.challenge, - }); - - if (isValid) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: '93b86c4b-72f9-40eb-9815-798928603d1e', - }); - return; - } - } else { - if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - return; - } - - const keys = await UserSecurityKeys.findBy({ - userId: user.id, - }); - - if (keys.length === 0) { - await fail(403, { - id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4', - }); - return; - } - - // 32 byte challenge - const challenge = randomBytes(32).toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); - - const challengeId = genId(); - - await AttestationChallenges.insert({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: false, - }); - - ctx.body = { - challenge, - challengeId, - securityKeys: keys.map(key => ({ - id: key.id, - })), - }; - ctx.status = 200; - return; - } - // never get here -}; diff --git a/packages/backend/src/server/api/private/signup-pending.ts b/packages/backend/src/server/api/private/signup-pending.ts deleted file mode 100644 index e5e39ba00d..0000000000 --- a/packages/backend/src/server/api/private/signup-pending.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Koa from 'koa'; -import { Users, UserPendings, UserProfiles } from '@/models/index.js'; -import { signup } from '../common/signup.js'; -import signin from '../common/signin.js'; - -export default async (ctx: Koa.Context) => { - const body = ctx.request.body; - - const code = body['code']; - - try { - const pendingUser = await UserPendings.findOneByOrFail({ code }); - - const { account, secret } = await signup({ - username: pendingUser.username, - passwordHash: pendingUser.password, - }); - - UserPendings.delete({ - id: pendingUser.id, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: account.id }); - - await UserProfiles.update({ userId: profile.userId }, { - email: pendingUser.email, - emailVerified: true, - emailVerifyCode: null, - }); - - signin(ctx, account); - } catch (e) { - ctx.throw(400, e); - } -}; diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts deleted file mode 100644 index 26f172637c..0000000000 --- a/packages/backend/src/server/api/private/signup.ts +++ /dev/null @@ -1,112 +0,0 @@ -import Koa from 'koa'; -import rndstr from 'rndstr'; -import bcrypt from 'bcryptjs'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; -import { Users, RegistrationTickets, UserPendings } from '@/models/index.js'; -import { signup } from '../common/signup.js'; -import config from '@/config/index.js'; -import { sendEmail } from '@/services/send-email.js'; -import { genId } from '@/misc/gen-id.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; - -export default async (ctx: Koa.Context) => { - const body = ctx.request.body; - - const instance = await fetchMeta(true); - - // Verify *Captcha - // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - } - - const username = body['username']; - const password = body['password']; - const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null; - const invitationCode = body['invitationCode']; - const emailAddress = body['emailAddress']; - - if (instance.emailRequiredForSignup) { - if (emailAddress == null || typeof emailAddress !== 'string') { - ctx.status = 400; - return; - } - - const available = await validateEmailForAccount(emailAddress); - if (!available) { - ctx.status = 400; - return; - } - } - - if (instance.disableRegistration) { - if (invitationCode == null || typeof invitationCode !== 'string') { - ctx.status = 400; - return; - } - - const ticket = await RegistrationTickets.findOneBy({ - code: invitationCode, - }); - - if (ticket == null) { - ctx.status = 400; - return; - } - - RegistrationTickets.delete(ticket.id); - } - - if (instance.emailRequiredForSignup) { - const code = rndstr('a-z0-9', 16); - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); - - await UserPendings.insert({ - id: genId(), - createdAt: new Date(), - code, - email: emailAddress, - username: username, - password: hash, - }); - - const link = `${config.url}/signup-complete/${code}`; - - sendEmail(emailAddress, 'Signup', - `To complete signup, please click this link:
${link}`, - `To complete signup, please click this link: ${link}`); - - ctx.status = 204; - } else { - try { - const { account, secret } = await signup({ - username, password, host, - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true, - }); - - (res as any).token = secret; - - ctx.body = res; - } catch (e) { - ctx.throw(400, e); - } - } -}; diff --git a/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts deleted file mode 100644 index 97cbcbecdb..0000000000 --- a/packages/backend/src/server/api/service/discord.ts +++ /dev/null @@ -1,287 +0,0 @@ -import Koa from 'koa'; -import Router from '@koa/router'; -import { OAuth2 } from 'oauth'; -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import { getJson } from '@/misc/fetch.js'; -import config from '@/config/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { redisClient } from '../../../db/redis.js'; -import signin from '../common/signin.js'; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): string { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/discord', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.discord; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = 'Discordの連携を解除しました :v:'; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); -}); - -async function getOAuth2() { - const meta = await fetchMeta(true); - - if (meta.enableDiscordIntegration) { - return new OAuth2( - meta.discordClientId!, - meta.discordClientSecret!, - 'https://discord.com/', - 'api/oauth2/authorize', - 'api/oauth2/token'); - } else { - return null; - } -} - -router.get('/connect/discord', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const params = { - redirect_uri: `${config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code', - }; - - redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/signin/discord', async ctx => { - const sessid = uuid(); - - const params = { - redirect_uri: `${config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code', - }; - - ctx.cookies.set('signin_with_discord_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true, - }); - - redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/dc/cb', async ctx => { - const userToken = getUserToken(ctx); - - const oauth2 = await getOAuth2(); - - if (!userToken) { - const sessid = ctx.cookies.get('signin_with_discord_sid'); - - if (!sessid) { - ctx.throw(400, 'invalid session'); - return; - } - - const code = ctx.query.code; - - if (!code || typeof code !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redisClient.get(sessid, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri, - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000, - }); - } - })); - - const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { - 'Authorization': `Bearer ${accessToken}`, - })) as Record; - - if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const profile = await UserProfiles.createQueryBuilder() - .where('"integrations"->\'discord\'->>\'id\' = :id', { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (profile == null) { - ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - await UserProfiles.update(profile.userId, { - integrations: { - ...profile.integrations, - discord: { - id: id, - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - username: username, - discriminator: discriminator, - }, - }, - }); - - signin(ctx, await Users.findOneBy({ id: profile.userId }) as ILocalUser, true); - } else { - const code = ctx.query.code; - - if (!code || typeof code !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redisClient.get(userToken, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri, - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000, - }); - } - })); - - const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { - 'Authorization': `Bearer ${accessToken}`, - })) as Record; - if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - discord: { - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - id: id, - username: username, - discriminator: discriminator, - }, - }, - }); - - ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts deleted file mode 100644 index 04dbd1f7ab..0000000000 --- a/packages/backend/src/server/api/service/github.ts +++ /dev/null @@ -1,259 +0,0 @@ -import Koa from 'koa'; -import Router from '@koa/router'; -import { OAuth2 } from 'oauth'; -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import { getJson } from '@/misc/fetch.js'; -import config from '@/config/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { redisClient } from '../../../db/redis.js'; -import signin from '../common/signin.js'; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): string { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/github', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.github; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = 'GitHubの連携を解除しました :v:'; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); -}); - -async function getOath2() { - const meta = await fetchMeta(true); - - if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) { - return new OAuth2( - meta.githubClientId, - meta.githubClientSecret, - 'https://github.com/', - 'login/oauth/authorize', - 'login/oauth/access_token'); - } else { - return null; - } -} - -router.get('/connect/github', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid(), - }; - - redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOath2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/signin/github', async ctx => { - const sessid = uuid(); - - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid(), - }; - - ctx.cookies.set('signin_with_github_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true, - }); - - redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOath2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/gh/cb', async ctx => { - const userToken = getUserToken(ctx); - - const oauth2 = await getOath2(); - - if (!userToken) { - const sessid = ctx.cookies.get('signin_with_github_sid'); - - if (!sessid) { - ctx.throw(400, 'invalid session'); - return; - } - - const code = ctx.query.code; - - if (!code || typeof code !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redisClient.get(sessid, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken(code, { - redirect_uri, - }, (err, accessToken, refresh, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ accessToken }); - } - })); - - const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { - 'Authorization': `bearer ${accessToken}`, - })) as Record; - if (typeof login !== 'string' || typeof id !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const link = await UserProfiles.createQueryBuilder() - .where('"integrations"->\'github\'->>\'id\' = :id', { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, await Users.findOneBy({ id: link.userId }) as ILocalUser, true); - } else { - const code = ctx.query.code; - - if (!code || typeof code !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redisClient.get(userToken, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ accessToken }); - } - })); - - const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { - 'Authorization': `bearer ${accessToken}`, - })) as Record; - - if (typeof login !== 'string' || typeof id !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - github: { - accessToken: accessToken, - id: id, - login: login, - }, - }, - }); - - ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/service/twitter.ts b/packages/backend/src/server/api/service/twitter.ts deleted file mode 100644 index 2b4f9f6daa..0000000000 --- a/packages/backend/src/server/api/service/twitter.ts +++ /dev/null @@ -1,201 +0,0 @@ -import Koa from 'koa'; -import Router from '@koa/router'; -import { v4 as uuid } from 'uuid'; -import autwh from 'autwh'; -import { IsNull } from 'typeorm'; -import { publishMainStream } from '@/services/stream.js'; -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import signin from '../common/signin.js'; -import { redisClient } from '../../../db/redis.js'; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): string { - return url == null ? '' : url.endsWith('/') ? url.substr(0, url.length - 1) : url; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/twitter', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (userToken == null) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.twitter; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = 'Twitterの連携を解除しました :v:'; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); -}); - -async function getTwAuth() { - const meta = await fetchMeta(true); - - if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) { - return autwh({ - consumerKey: meta.twitterConsumerKey, - consumerSecret: meta.twitterConsumerSecret, - callbackUrl: `${config.url}/api/tw/cb`, - }); - } else { - return null; - } -} - -router.get('/connect/twitter', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (userToken == null) { - ctx.throw(400, 'signin required'); - return; - } - - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - redisClient.set(userToken, JSON.stringify(twCtx)); - ctx.redirect(twCtx.url); -}); - -router.get('/signin/twitter', async ctx => { - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - - const sessid = uuid(); - - redisClient.set(sessid, JSON.stringify(twCtx)); - - ctx.cookies.set('signin_with_twitter_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true, - }); - - ctx.redirect(twCtx.url); -}); - -router.get('/tw/cb', async ctx => { - const userToken = getUserToken(ctx); - - const twAuth = await getTwAuth(); - - if (userToken == null) { - const sessid = ctx.cookies.get('signin_with_twitter_sid'); - - if (sessid == null) { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise((res, rej) => { - redisClient.get(sessid, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const verifier = ctx.query.oauth_verifier; - if (!verifier || typeof verifier !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const result = await twAuth!.done(JSON.parse(twCtx), verifier); - - const link = await UserProfiles.createQueryBuilder() - .where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, await Users.findOneBy({ id: link.userId }) as ILocalUser, true); - } else { - const verifier = ctx.query.oauth_verifier; - - if (!verifier || typeof verifier !== 'string') { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise((res, rej) => { - redisClient.get(userToken, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth!.done(JSON.parse(twCtx), verifier); - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName, - }, - }, - }); - - ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true, - })); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts new file mode 100644 index 0000000000..d6005b1ee8 --- /dev/null +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; +import { LocalTimelineChannelService } from './channels/local-timeline.js'; +import { HomeTimelineChannelService } from './channels/home-timeline.js'; +import { GlobalTimelineChannelService } from './channels/global-timeline.js'; +import { MainChannelService } from './channels/main.js'; +import { ChannelChannelService } from './channels/channel.js'; +import { AdminChannelService } from './channels/admin.js'; +import { ServerStatsChannelService } from './channels/server-stats.js'; +import { QueueStatsChannelService } from './channels/queue-stats.js'; +import { UserListChannelService } from './channels/user-list.js'; +import { AntennaChannelService } from './channels/antenna.js'; +import { MessagingChannelService } from './channels/messaging.js'; +import { MessagingIndexChannelService } from './channels/messaging-index.js'; +import { DriveChannelService } from './channels/drive.js'; +import { HashtagChannelService } from './channels/hashtag.js'; + +@Injectable() +export class ChannelsService { + constructor( + private mainChannelService: MainChannelService, + private homeTimelineChannelService: HomeTimelineChannelService, + private localTimelineChannelService: LocalTimelineChannelService, + private hybridTimelineChannelService: HybridTimelineChannelService, + private globalTimelineChannelService: GlobalTimelineChannelService, + private userListChannelService: UserListChannelService, + private hashtagChannelService: HashtagChannelService, + private antennaChannelService: AntennaChannelService, + private channelChannelService: ChannelChannelService, + private messagingChannelService: MessagingChannelService, + private messagingIndexChannelService: MessagingIndexChannelService, + private driveChannelService: DriveChannelService, + private serverStatsChannelService: ServerStatsChannelService, + private queueStatsChannelService: QueueStatsChannelService, + private adminChannelService: AdminChannelService, + ) { + } + + public getChannelService(name: string) { + switch (name) { + case 'main': return this.mainChannelService; + case 'homeTimeline': return this.homeTimelineChannelService; + case 'localTimeline': return this.localTimelineChannelService; + case 'hybridTimeline': return this.hybridTimelineChannelService; + case 'globalTimeline': return this.globalTimelineChannelService; + case 'userList': return this.userListChannelService; + case 'hashtag': return this.hashtagChannelService; + case 'antenna': return this.antennaChannelService; + case 'channel': return this.channelChannelService; + case 'messaging': return this.messagingChannelService; + case 'messagingIndex': return this.messagingIndexChannelService; + case 'drive': return this.driveChannelService; + case 'serverStats': return this.serverStatsChannelService; + case 'queueStats': return this.queueStatsChannelService; + case 'admin': return this.adminChannelService; + + default: + throw new Error(`no such channel: ${name}`); + } + } +} diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index d2cc5122d5..5480c12c09 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -1,4 +1,4 @@ -import Connection from '.'; +import type Connection from '.'; /** * Stream channel diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 945182ea10..8c3c0d2adf 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,6 +1,7 @@ +import { Inject, Injectable } from '@nestjs/common'; import Channel from '../channel.js'; -export default class extends Channel { +class AdminChannel extends Channel { public readonly chName = 'admin'; public static shouldShare = true; public static requireCredential = true; @@ -12,3 +13,20 @@ export default class extends Channel { }); } } + +@Injectable() +export class AdminChannelService { + public readonly shouldShare = AdminChannel.shouldShare; + public readonly requireCredential = AdminChannel.requireCredential; + + constructor( + ) { + } + + public create(id: string, connection: Channel['connection']): AdminChannel { + return new AdminChannel( + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index d28320d928..7c34aef495 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,15 +1,22 @@ -import Channel from '../channel.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { StreamMessages } from '../types.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import Channel from '../channel.js'; +import type { StreamMessages } from '../types.js'; -export default class extends Channel { +class AntennaChannel extends Channel { public readonly chName = 'antenna'; public static shouldShare = false; public static requireCredential = false; private antennaId: string; - constructor(id: string, connection: Channel['connection']) { + constructor( + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onEvent = this.onEvent.bind(this); } @@ -23,7 +30,7 @@ export default class extends Channel { private async onEvent(data: StreamMessages['antenna']['payload']) { if (data.type === 'note') { - const note = await Notes.pack(data.body.id, this.user, { detail: true }); + const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.muting)) return; @@ -43,3 +50,22 @@ export default class extends Channel { this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent); } } + +@Injectable() +export class AntennaChannelService { + public readonly shouldShare = AntennaChannel.shouldShare; + public readonly requireCredential = AntennaChannel.requireCredential; + + constructor( + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): AntennaChannel { + return new AntennaChannel( + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 3cdd89a8b3..2ef70e62e9 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,11 +1,14 @@ -import Channel from '../channel.js'; -import { Notes, Users } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { User } from '@/models/entities/user.js'; -import { StreamMessages } from '../types.js'; -import { Packed } from '@/misc/schema.js'; +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 Channel from '../channel.js'; +import type { StreamMessages } from '../types.js'; -export default class extends Channel { +class ChannelChannel extends Channel { public readonly chName = 'channel'; public static shouldShare = false; public static requireCredential = false; @@ -13,7 +16,13 @@ export default class extends Channel { private typers: Record = {}; private emitTypersIntervalId: ReturnType; - constructor(id: string, connection: Channel['connection']) { + constructor( + private noteEntityService: NoteEntityService, + private userEntityService: UserEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); this.emitTypers = this.emitTypers.bind(this); @@ -33,13 +42,13 @@ export default class extends Channel { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { detail: true, }); } @@ -73,7 +82,7 @@ export default class extends Channel { if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; } - const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); + const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); this.send({ type: 'typers', @@ -89,3 +98,24 @@ export default class extends Channel { clearInterval(this.emitTypersIntervalId); } } + +@Injectable() +export class ChannelChannelService { + public readonly shouldShare = ChannelChannel.shouldShare; + public readonly requireCredential = ChannelChannel.requireCredential; + + constructor( + private noteEntityService: NoteEntityService, + private userEntityService: UserEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): ChannelChannel { + return new ChannelChannel( + this.noteEntityService, + this.userEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 140255acd1..80d83cd690 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,6 +1,7 @@ +import { Inject, Injectable } from '@nestjs/common'; import Channel from '../channel.js'; -export default class extends Channel { +class DriveChannel extends Channel { public readonly chName = 'drive'; public static shouldShare = true; public static requireCredential = true; @@ -12,3 +13,20 @@ export default class extends Channel { }); } } + +@Injectable() +export class DriveChannelService { + public readonly shouldShare = DriveChannel.shouldShare; + public readonly requireCredential = DriveChannel.requireCredential; + + constructor( + ) { + } + + public create(id: string, connection: Channel['connection']): DriveChannel { + return new DriveChannel( + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 5b4ae850ec..a8617582dc 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,23 +1,31 @@ -import Channel from '../channel.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/schema.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import Channel from '../channel.js'; -export default class extends Channel { +class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; - constructor(id: string, connection: Channel['connection']) { + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await this.metaService.fetch(); if (meta.disableGlobalTimeline) { if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; } @@ -32,13 +40,13 @@ export default class extends Channel { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { detail: true, }); } @@ -75,3 +83,24 @@ export default class extends Channel { this.subscriber.off('notesStream', this.onNote); } } + +@Injectable() +export class GlobalTimelineChannelService { + public readonly shouldShare = GlobalTimelineChannel.shouldShare; + public readonly requireCredential = GlobalTimelineChannel.requireCredential; + + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): GlobalTimelineChannel { + return new GlobalTimelineChannel( + this.metaService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 741db447e6..0f6c081c12 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,16 +1,23 @@ -import Channel from '../channel.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/schema.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import Channel from '../channel.js'; -export default class extends Channel { +class HashtagChannel extends Channel { public readonly chName = 'hashtag'; public static shouldShare = false; public static requireCredential = false; private q: string[][]; - constructor(id: string, connection: Channel['connection']) { + constructor( + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); } @@ -31,7 +38,7 @@ export default class extends Channel { // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { detail: true, }); } @@ -51,3 +58,22 @@ export default class extends Channel { this.subscriber.off('notesStream', this.onNote); } } + +@Injectable() +export class HashtagChannelService { + public readonly shouldShare = HashtagChannel.shouldShare; + public readonly requireCredential = HashtagChannel.requireCredential; + + constructor( + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): HashtagChannel { + return new HashtagChannel( + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 075a242ef0..16e0cebc72 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,16 +1,23 @@ -import Channel from '../channel.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/schema.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import Channel from '../channel.js'; -export default class extends Channel { +class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; - constructor(id: string, connection: Channel['connection']) { + constructor( + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); } @@ -32,7 +39,7 @@ export default class extends Channel { if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user!, { + note = await this.noteEntityService.pack(note.id, this.user!, { detail: true, }); @@ -42,13 +49,13 @@ export default class extends Channel { } else { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user!, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user!, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user!, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, { detail: true, }); } @@ -83,3 +90,22 @@ export default class extends Channel { this.subscriber.off('notesStream', this.onNote); } } + +@Injectable() +export class HomeTimelineChannelService { + public readonly shouldShare = HomeTimelineChannel.shouldShare; + public readonly requireCredential = HomeTimelineChannel.requireCredential; + + constructor( + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): HomeTimelineChannel { + return new HomeTimelineChannel( + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index f5dedf77ce..f1ce822583 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,23 +1,32 @@ -import Channel from '../channel.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { Packed } from '@/misc/schema.js'; +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 Channel from '../channel.js'; -export default class extends Channel { +class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = true; public static requireCredential = true; - constructor(id: string, connection: Channel['connection']) { + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); } - public async init(params: any) { - const meta = await fetchMeta(); + public async init(params: any): Promise { + const meta = await this.metaService.fetch(); if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; // Subscribe events @@ -37,7 +46,7 @@ export default class extends Channel { )) return; if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user!, { + note = await this.noteEntityService.pack(note.id, this.user!, { detail: true, }); @@ -47,13 +56,13 @@ export default class extends Channel { } else { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user!, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user!, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user!, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, { detail: true, }); } @@ -86,8 +95,29 @@ export default class extends Channel { this.send('note', note); } - public dispose() { + public dispose(): void { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); } } + +@Injectable() +export class HybridTimelineChannelService { + public readonly shouldShare = HybridTimelineChannel.shouldShare; + public readonly requireCredential = HybridTimelineChannel.requireCredential; + + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): HybridTimelineChannel { + return new HybridTimelineChannel( + this.metaService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/index.ts b/packages/backend/src/server/api/stream/channels/index.ts deleted file mode 100644 index d422edde87..0000000000 --- a/packages/backend/src/server/api/stream/channels/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import main from './main.js'; -import homeTimeline from './home-timeline.js'; -import localTimeline from './local-timeline.js'; -import hybridTimeline from './hybrid-timeline.js'; -import globalTimeline from './global-timeline.js'; -import serverStats from './server-stats.js'; -import queueStats from './queue-stats.js'; -import userList from './user-list.js'; -import antenna from './antenna.js'; -import messaging from './messaging.js'; -import messagingIndex from './messaging-index.js'; -import drive from './drive.js'; -import hashtag from './hashtag.js'; -import channel from './channel.js'; -import admin from './admin.js'; - -export default { - main, - homeTimeline, - localTimeline, - hybridTimeline, - globalTimeline, - serverStats, - queueStats, - userList, - antenna, - messaging, - messagingIndex, - drive, - hashtag, - channel, - admin, -}; 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 f01f477238..5a5a43f845 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,22 +1,30 @@ -import Channel from '../channel.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/schema.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import Channel from '../channel.js'; -export default class extends Channel { +class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; - constructor(id: string, connection: Channel['connection']) { + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onNote = this.onNote.bind(this); } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await this.metaService.fetch(); if (meta.disableLocalTimeline) { if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; } @@ -32,13 +40,13 @@ export default class extends Channel { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { detail: true, }); } @@ -72,3 +80,24 @@ export default class extends Channel { this.subscriber.off('notesStream', this.onNote); } } + +@Injectable() +export class LocalTimelineChannelService { + public readonly shouldShare = LocalTimelineChannel.shouldShare; + public readonly requireCredential = LocalTimelineChannel.requireCredential; + + constructor( + private metaService: MetaService, + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): LocalTimelineChannel { + return new LocalTimelineChannel( + this.metaService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 9cfea0bfc4..12908e07b4 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,12 +1,23 @@ -import Channel from '../channel.js'; -import { Notes } from '@/models/index.js'; +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 Channel from '../channel.js'; -export default class extends Channel { +class MainChannel extends Channel { public readonly chName = 'main'; public static shouldShare = true; public static requireCredential = true; + constructor( + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + public async init(params: any) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { @@ -17,7 +28,7 @@ export default class extends Channel { if (data.body.userId && this.muting.has(data.body.userId)) return; if (data.body.note && data.body.note.isHidden) { - const note = await Notes.pack(data.body.note.id, this.user, { + const note = await this.noteEntityService.pack(data.body.note.id, this.user, { detail: true, }); this.connection.cacheNote(note); @@ -30,7 +41,7 @@ export default class extends Channel { if (this.muting.has(data.body.userId)) return; if (data.body.isHidden) { - const note = await Notes.pack(data.body.id, this.user, { + const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true, }); this.connection.cacheNote(note); @@ -44,3 +55,22 @@ export default class extends Channel { }); } } + +@Injectable() +export class MainChannelService { + public readonly shouldShare = MainChannel.shouldShare; + public readonly requireCredential = MainChannel.requireCredential; + + constructor( + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): MainChannel { + return new MainChannel( + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts index b930785d20..bebc07f4ad 100644 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ b/packages/backend/src/server/api/stream/channels/messaging-index.ts @@ -1,6 +1,7 @@ +import { Inject, Injectable } from '@nestjs/common'; import Channel from '../channel.js'; -export default class extends Channel { +class MessagingIndexChannel extends Channel { public readonly chName = 'messagingIndex'; public static shouldShare = true; public static requireCredential = true; @@ -12,3 +13,20 @@ export default class extends Channel { }); } } + +@Injectable() +export class MessagingIndexChannelService { + public readonly shouldShare = MessagingIndexChannel.shouldShare; + public readonly requireCredential = MessagingIndexChannel.requireCredential; + + constructor( + ) { + } + + public create(id: string, connection: Channel['connection']): MessagingIndexChannel { + return new MessagingIndexChannel( + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index 877d44c38e..5bf20c4101 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -1,11 +1,14 @@ -import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js'; +import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +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 Channel from '../channel.js'; -import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index.js'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { StreamMessages } from '../types.js'; +import type { StreamMessages } from '../types.js'; -export default class extends Channel { +class MessagingChannel extends Channel { public readonly chName = 'messaging'; public static shouldShare = false; public static requireCredential = true; @@ -17,7 +20,16 @@ export default class extends Channel { private typers: Record = {}; private emitTypersIntervalId: ReturnType; - constructor(id: string, connection: Channel['connection']) { + constructor( + private usersRepository: UsersRepository, + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + private messagingMessagesRepository: MessagingMessagesRepository, + private userEntityService: UserEntityService, + private messagingService: MessagingService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.onEvent = this.onEvent.bind(this); this.onMessage = this.onMessage.bind(this); @@ -26,12 +38,12 @@ export default class extends Channel { public async init(params: any) { this.otherpartyId = params.otherparty; - this.otherparty = this.otherpartyId ? await Users.findOneByOrFail({ id: this.otherpartyId }) : null; + this.otherparty = this.otherpartyId ? await this.usersRepository.findOneByOrFail({ id: this.otherpartyId }) : null; this.groupId = params.group; // Check joining if (this.groupId) { - const joining = await UserGroupJoinings.findOneBy({ + const joining = await this.userGroupJoiningsRepository.findOneBy({ userId: this.user!.id, userGroupId: this.groupId, }); @@ -68,16 +80,16 @@ export default class extends Channel { switch (type) { case 'read': if (this.otherpartyId) { - readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); + this.messagingService.readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); // リモートユーザーからのメッセージだったら既読配信 - if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) { - MessagingMessages.findOneBy({ id: body.id }).then(message => { - if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message); + if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) { + this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => { + if (message) this.messagingService.deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message); }); } } else if (this.groupId) { - readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); + this.messagingService.readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); } break; } @@ -91,7 +103,7 @@ export default class extends Channel { if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; } - const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); + const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); this.send({ type: 'typers', @@ -105,3 +117,36 @@ export default class extends Channel { clearInterval(this.emitTypersIntervalId); } } + +@Injectable() +export class MessagingChannelService { + public readonly shouldShare = MessagingChannel.shouldShare; + public readonly requireCredential = MessagingChannel.requireCredential; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, + + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, + + private userEntityService: UserEntityService, + private messagingService: MessagingService, + ) { + } + + public create(id: string, connection: Channel['connection']): MessagingChannel { + return new MessagingChannel( + this.usersRepository, + this.userGroupJoiningsRepository, + this.messagingMessagesRepository, + this.userEntityService, + this.messagingService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index b67600474b..1802c6723b 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,9 +1,10 @@ import Xev from 'xev'; +import { Inject, Injectable } from '@nestjs/common'; import Channel from '../channel.js'; const ev = new Xev(); -export default class extends Channel { +class QueueStatsChannel extends Channel { public readonly chName = 'queueStats'; public static shouldShare = true; public static requireCredential = false; @@ -40,3 +41,20 @@ export default class extends Channel { ev.removeListener('queueStats', this.onStats); } } + +@Injectable() +export class QueueStatsChannelService { + public readonly shouldShare = QueueStatsChannel.shouldShare; + public readonly requireCredential = QueueStatsChannel.requireCredential; + + constructor( + ) { + } + + public create(id: string, connection: Channel['connection']): QueueStatsChannel { + return new QueueStatsChannel( + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index db75a6fa38..e2b00de25f 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,9 +1,10 @@ import Xev from 'xev'; +import { Inject, Injectable } from '@nestjs/common'; import Channel from '../channel.js'; const ev = new Xev(); -export default class extends Channel { +class ServerStatsChannel extends Channel { public readonly chName = 'serverStats'; public static shouldShare = true; public static requireCredential = false; @@ -40,3 +41,20 @@ export default class extends Channel { ev.removeListener('serverStats', this.onStats); } } + +@Injectable() +export class ServerStatsChannelService { + public readonly shouldShare = ServerStatsChannel.shouldShare; + public readonly requireCredential = ServerStatsChannel.requireCredential; + + constructor( + ) { + } + + public create(id: string, connection: Channel['connection']): ServerStatsChannel { + return new ServerStatsChannel( + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 97ad2983c5..a45c7d9468 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,10 +1,14 @@ -import Channel from '../channel.js'; -import { Notes, UserListJoinings, UserLists } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/schema.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import Channel from '../channel.js'; -export default class extends Channel { +class UserListChannel extends Channel { public readonly chName = 'userList'; public static shouldShare = false; public static requireCredential = false; @@ -12,7 +16,14 @@ export default class extends Channel { public listUsers: User['id'][] = []; private listUsersClock: NodeJS.Timer; - constructor(id: string, connection: Channel['connection']) { + constructor( + private userListsRepository: UserListsRepository, + private userListJoiningsRepository: UserListJoiningsRepository, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { super(id, connection); this.updateListUsers = this.updateListUsers.bind(this); this.onNote = this.onNote.bind(this); @@ -22,7 +33,7 @@ export default class extends Channel { this.listId = params.listId as string; // Check existence and owner - const list = await UserLists.findOneBy({ + const list = await this.userListsRepository.findOneBy({ id: this.listId, userId: this.user!.id, }); @@ -38,7 +49,7 @@ export default class extends Channel { } private async updateListUsers() { - const users = await UserListJoinings.find({ + const users = await this.userListJoiningsRepository.find({ where: { userListId: this.listId, }, @@ -52,7 +63,7 @@ export default class extends Channel { if (!this.listUsers.includes(note.userId)) return; if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user, { + note = await this.noteEntityService.pack(note.id, this.user, { detail: true, }); @@ -62,13 +73,13 @@ export default class extends Channel { } else { // リプライなら再pack if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { detail: true, }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { detail: true, }); } @@ -90,3 +101,30 @@ export default class extends Channel { clearInterval(this.listUsersClock); } } + +@Injectable() +export class UserListChannelService { + public readonly shouldShare = UserListChannel.shouldShare; + public readonly requireCredential = UserListChannel.requireCredential; + + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private noteEntityService: NoteEntityService, + ) { + } + + public create(id: string, connection: Channel['connection']): UserListChannel { + return new UserListChannel( + this.userListsRepository, + this.userListJoiningsRepository, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 2d23145f14..0c5066b736 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,18 +1,18 @@ -import { EventEmitter } from 'events'; -import * as websocket from 'websocket'; -import readNote from '@/services/note/read.js'; -import { User } from '@/models/entities/user.js'; -import { Channel as ChannelModel } from '@/models/entities/channel.js'; -import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { Packed } from '@/misc/schema.js'; -import { readNotification } from '../common/read-notification.js'; -import channels from './channels/index.js'; -import Channel from './channel.js'; -import { StreamEventEmitter, StreamMessages } from './types.js'; +import type { User } from '@/models/entities/User.js'; +import type { Channel as ChannelModel } from '@/models/entities/Channel.js'; +import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +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 type { ChannelsService } from './ChannelsService.js'; +import type * as websocket from 'websocket'; +import type { EventEmitter } from 'events'; +import type Channel from './channel.js'; +import type { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection @@ -32,6 +32,16 @@ export default class Connection { private cachedNotes: Packed<'Note'>[] = []; constructor( + private followingsRepository: FollowingsRepository, + private mutingsRepository: MutingsRepository, + private blockingsRepository: BlockingsRepository, + private channelFollowingsRepository: ChannelFollowingsRepository, + private userProfilesRepository: UserProfilesRepository, + private channelsService: ChannelsService, + private globalEventService: GlobalEventService, + private noteReadService: NoteReadService, + private notificationService: NotificationService, + wsConnection: websocket.connection, subscriber: EventEmitter, user: User | null | undefined, @@ -173,7 +183,7 @@ export default class Connection { if (note == null) return; if (this.user && (note.userId !== this.user.id)) { - readNote(this.user.id, [note], { + this.noteReadService.read(this.user.id, [note], { following: this.following, followingChannels: this.followingChannels, }); @@ -182,7 +192,7 @@ export default class Connection { private onReadNotification(payload: any) { if (!payload.id) return; - readNotification(this.user!.id, [payload.id]); + this.notificationService.readNotification(this.user!.id, [payload.id]); } /** @@ -253,16 +263,18 @@ export default class Connection { * チャンネルに接続 */ public connectChannel(id: string, params: any, channel: string, pong = false) { - if ((channels as any)[channel].requireCredential && this.user == null) { + const channelService = this.channelsService.getChannelService(channel); + + if (channelService.requireCredential && this.user == null) { return; } // 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視 - if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) { + if (channelService.shouldShare && this.channels.some(c => c.chName === channel)) { return; } - const ch: Channel = new (channels as any)[channel](id, this); + const ch: Channel = channelService.create(id, this); this.channels.push(ch); ch.init(params); @@ -299,22 +311,22 @@ export default class Connection { private typingOnChannel(channel: ChannelModel['id']) { if (this.user) { - publishChannelStream(channel, 'typing', this.user.id); + this.globalEventService.publishChannelStream(channel, 'typing', this.user.id); } } private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) { if (this.user) { if (param.partner) { - publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id); + this.globalEventService.publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id); } else if (param.group) { - publishGroupMessagingStream(param.group, 'typing', this.user.id); + this.globalEventService.publishGroupMessagingStream(param.group, 'typing', this.user.id); } } } private async updateFollowing() { - const followings = await Followings.find({ + const followings = await this.followingsRepository.find({ where: { followerId: this.user!.id, }, @@ -325,7 +337,7 @@ export default class Connection { } private async updateMuting() { - const mutings = await Mutings.find({ + const mutings = await this.mutingsRepository.find({ where: { muterId: this.user!.id, }, @@ -336,7 +348,7 @@ export default class Connection { } private async updateBlocking() { // ここでいうBlockingは被Blockingの意 - const blockings = await Blockings.find({ + const blockings = await this.blockingsRepository.find({ where: { blockeeId: this.user!.id, }, @@ -347,7 +359,7 @@ export default class Connection { } private async updateFollowingChannels() { - const followings = await ChannelFollowings.find({ + const followings = await this.channelFollowingsRepository.find({ where: { followerId: this.user!.id, }, @@ -358,7 +370,7 @@ export default class Connection { } private async updateUserProfile() { - this.userProfile = await UserProfiles.findOneBy({ + this.userProfile = await this.userProfilesRepository.findOneBy({ userId: this.user!.id, }); } diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 3b0a75d793..000f9a25dd 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -1,21 +1,20 @@ -import { EventEmitter } from 'events'; -import Emitter from 'strict-event-emitter-types'; -import { Channel } from '@/models/entities/channel.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { Note } from '@/models/entities/note.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; -import { Signin } from '@/models/entities/signin.js'; -import { Page } from '@/models/entities/page.js'; -import { Packed } from '@/misc/schema.js'; -import { Webhook } from '@/models/entities/webhook'; +import type { Channel } from '@/models/entities/Channel.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; +import type { UserGroup } from '@/models/entities/UserGroup.js'; +import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import type { Signin } from '@/models/entities/Signin.js'; +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 Emitter from 'strict-event-emitter-types'; +import type { EventEmitter } from 'events'; //#region Stream type-body definitions export interface InternalStreamTypes { diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts deleted file mode 100644 index f8e42d27fe..0000000000 --- a/packages/backend/src/server/api/streaming.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as http from 'node:http'; -import * as websocket from 'websocket'; - -import MainStreamConnection from './stream/index.js'; -import { ParsedUrlQuery } from 'querystring'; -import authenticate from './authenticate.js'; -import { EventEmitter } from 'events'; -import { subsdcriber as redisClient } from '../../db/redis.js'; -import { Users } from '@/models/index.js'; - -export const initializeStreamingServer = (server: http.Server) => { - // Init websocket server - const ws = new websocket.server({ - httpServer: server, - }); - - ws.on('request', async (request) => { - const q = request.resourceURL.query as ParsedUrlQuery; - - // TODO: トークンが間違ってるなどしてauthenticateに失敗したら - // コネクション切断するなりエラーメッセージ返すなりする - // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので) - const [user, app] = await authenticate(q.i as string); - - if (user?.isSuspended) { - request.reject(400); - return; - } - - const connection = request.accept(); - - const ev = new EventEmitter(); - - async function onRedisMessage(_: string, data: string) { - const parsed = JSON.parse(data); - ev.emit(parsed.channel, parsed.message); - } - - redisClient.on('message', onRedisMessage); - - const main = new MainStreamConnection(connection, ev, user, app); - - const intervalId = user ? setInterval(() => { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - }, 1000 * 60 * 5) : null; - if (user) { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - } - - connection.once('close', () => { - ev.removeAllListeners(); - main.dispose(); - redisClient.off('message', onRedisMessage); - if (intervalId) clearInterval(intervalId); - }); - - connection.on('message', async (data) => { - if (data.type === 'utf8' && data.utf8Data === 'ping') { - connection.send('pong'); - } - }); - }); -}; diff --git a/packages/backend/src/server/assets/bad-egg.png b/packages/backend/src/server/assets/bad-egg.png new file mode 100644 index 0000000000..e96ba0dcc1 Binary files /dev/null and b/packages/backend/src/server/assets/bad-egg.png differ diff --git a/packages/backend/src/server/assets/cache-expired.png b/packages/backend/src/server/assets/cache-expired.png new file mode 100644 index 0000000000..5d988c502b Binary files /dev/null and b/packages/backend/src/server/assets/cache-expired.png differ diff --git a/packages/backend/src/server/assets/dummy.png b/packages/backend/src/server/assets/dummy.png new file mode 100644 index 0000000000..39332b0c1b Binary files /dev/null and b/packages/backend/src/server/assets/dummy.png differ diff --git a/packages/backend/src/server/assets/not-an-image.png b/packages/backend/src/server/assets/not-an-image.png new file mode 100644 index 0000000000..39e4aa0892 Binary files /dev/null and b/packages/backend/src/server/assets/not-an-image.png differ diff --git a/packages/backend/src/server/assets/thumbnail-not-available.png b/packages/backend/src/server/assets/thumbnail-not-available.png new file mode 100644 index 0000000000..07cad9919c Binary files /dev/null and b/packages/backend/src/server/assets/thumbnail-not-available.png differ diff --git a/packages/backend/src/server/assets/tombstone.png b/packages/backend/src/server/assets/tombstone.png new file mode 100644 index 0000000000..83159d6b3c Binary files /dev/null and b/packages/backend/src/server/assets/tombstone.png differ diff --git a/packages/backend/src/server/file/assets/bad-egg.png b/packages/backend/src/server/file/assets/bad-egg.png deleted file mode 100644 index e96ba0dcc1..0000000000 Binary files a/packages/backend/src/server/file/assets/bad-egg.png and /dev/null differ diff --git a/packages/backend/src/server/file/assets/cache-expired.png b/packages/backend/src/server/file/assets/cache-expired.png deleted file mode 100644 index 5d988c502b..0000000000 Binary files a/packages/backend/src/server/file/assets/cache-expired.png and /dev/null differ diff --git a/packages/backend/src/server/file/assets/dummy.png b/packages/backend/src/server/file/assets/dummy.png deleted file mode 100644 index 39332b0c1b..0000000000 Binary files a/packages/backend/src/server/file/assets/dummy.png and /dev/null differ diff --git a/packages/backend/src/server/file/assets/not-an-image.png b/packages/backend/src/server/file/assets/not-an-image.png deleted file mode 100644 index 39e4aa0892..0000000000 Binary files a/packages/backend/src/server/file/assets/not-an-image.png and /dev/null differ diff --git a/packages/backend/src/server/file/assets/thumbnail-not-available.png b/packages/backend/src/server/file/assets/thumbnail-not-available.png deleted file mode 100644 index 07cad9919c..0000000000 Binary files a/packages/backend/src/server/file/assets/thumbnail-not-available.png and /dev/null differ diff --git a/packages/backend/src/server/file/assets/tombstone.png b/packages/backend/src/server/file/assets/tombstone.png deleted file mode 100644 index 83159d6b3c..0000000000 Binary files a/packages/backend/src/server/file/assets/tombstone.png and /dev/null differ diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts deleted file mode 100644 index 07a493700a..0000000000 --- a/packages/backend/src/server/file/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * File Server - */ - -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import Koa from 'koa'; -import cors from '@koa/cors'; -import Router from '@koa/router'; -import sendDriveFile from './send-drive-file.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -// Init app -const app = new Koa(); -app.use(cors()); -app.use(async (ctx, next) => { - ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); - await next(); -}); - -// Init router -const router = new Router(); - -router.get('/app-default.jpg', ctx => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); - ctx.body = file; - ctx.set('Content-Type', 'image/jpeg'); - ctx.set('Cache-Control', 'max-age=31536000, immutable'); -}); - -router.get('/:key', sendDriveFile); -router.get('/:key/(.*)', sendDriveFile); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts deleted file mode 100644 index c34e043145..0000000000 --- a/packages/backend/src/server/file/send-drive-file.ts +++ /dev/null @@ -1,126 +0,0 @@ -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import Koa from 'koa'; -import send from 'koa-send'; -import rename from 'rename'; -import { serverLogger } from '../index.js'; -import { contentDisposition } from '@/misc/content-disposition.js'; -import { DriveFiles } from '@/models/index.js'; -import { InternalStorage } from '@/services/drive/internal-storage.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { detectType } from '@/misc/get-file-info.js'; -import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js'; -import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail.js'; -import { StatusError } from '@/misc/fetch.js'; -import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const assets = `${_dirname}/../../server/file/assets/`; - -const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { - serverLogger.error(e); - ctx.status = 500; - ctx.set('Cache-Control', 'max-age=300'); -}; - -// eslint-disable-next-line import/no-default-export -export default async function(ctx: Koa.Context) { - const key = ctx.params.key; - - // Fetch drive file - const file = await DriveFiles.createQueryBuilder('file') - .where('file.accessKey = :accessKey', { accessKey: key }) - .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key }) - .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key }) - .getOne(); - - if (file == null) { - ctx.status = 404; - ctx.set('Cache-Control', 'max-age=86400'); - await send(ctx as any, '/dummy.png', { root: assets }); - return; - } - - const isThumbnail = file.thumbnailAccessKey === key; - const isWebpublic = file.webpublicAccessKey === key; - - if (!file.storedInternal) { - if (file.isLink && file.uri) { // 期限切れリモートファイル - const [path, cleanup] = await createTemp(); - - try { - await downloadUrl(file.uri, path); - - const { mime, ext } = await detectType(path); - - const convertFile = async () => { - if (isThumbnail) { - if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(mime)) { - return await convertToWebp(path, 498, 280); - } else if (mime.startsWith('video/')) { - return await GenerateVideoThumbnail(path); - } - } - - if (isWebpublic) { - if (['image/svg+xml'].includes(mime)) { - return await convertToPng(path, 2048, 2048); - } - } - - return { - data: fs.readFileSync(path), - ext, - type: mime, - }; - }; - - const image = await convertFile(); - ctx.body = image.data; - ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - ctx.set('Cache-Control', 'max-age=31536000, immutable'); - } catch (e) { - serverLogger.error(`${e}`); - - if (e instanceof StatusError && e.isClientError) { - ctx.status = e.statusCode; - ctx.set('Cache-Control', 'max-age=86400'); - } else { - ctx.status = 500; - ctx.set('Cache-Control', 'max-age=300'); - } - } finally { - cleanup(); - } - return; - } - - ctx.status = 204; - ctx.set('Cache-Control', 'max-age=86400'); - return; - } - - if (isThumbnail || isWebpublic) { - const { mime, ext } = await detectType(InternalStorage.resolvePath(key)); - const filename = rename(file.name, { - suffix: isThumbnail ? '-thumb' : '-web', - extname: ext ? `.${ext}` : undefined, - }).toString(); - - ctx.body = InternalStorage.read(key); - ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream'); - ctx.set('Cache-Control', 'max-age=31536000, immutable'); - ctx.set('Content-Disposition', contentDisposition('inline', filename)); - } else { - const readable = InternalStorage.read(file.accessKey!); - readable.on('error', commonReadableHandlerGenerator(ctx)); - ctx.body = readable; - ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream'); - ctx.set('Cache-Control', 'max-age=31536000, immutable'); - ctx.set('Content-Disposition', contentDisposition('inline', file.name)); - } -} diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts deleted file mode 100644 index f31de2b7f4..0000000000 --- a/packages/backend/src/server/index.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Core Server - */ - -import cluster from 'node:cluster'; -import * as fs from 'node:fs'; -import * as http from 'node:http'; -import Koa from 'koa'; -import Router from '@koa/router'; -import mount from 'koa-mount'; -import koaLogger from 'koa-logger'; -import * as slow from 'koa-slow'; - -import { IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import Logger from '@/services/logger.js'; -import { UserProfiles, Users } from '@/models/index.js'; -import { genIdenticon } from '@/misc/gen-identicon.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { publishMainStream } from '@/services/stream.js'; -import * as Acct from '@/misc/acct.js'; -import { envOption } from '../env.js'; -import activityPub from './activitypub.js'; -import nodeinfo from './nodeinfo.js'; -import wellKnown from './well-known.js'; -import apiServer from './api/index.js'; -import fileServer from './file/index.js'; -import proxyServer from './proxy/index.js'; -import webServer from './web/index.js'; -import { initializeStreamingServer } from './api/streaming.js'; - -export const serverLogger = new Logger('server', 'gray', false); - -// Init app -const app = new Koa(); -app.proxy = true; - -if (!['production', 'test'].includes(process.env.NODE_ENV || '')) { - // Logger - app.use(koaLogger(str => { - serverLogger.info(str); - })); - - // Delay - if (envOption.slow) { - app.use(slow({ - delay: 3000, - })); - } -} - -// HSTS -// 6months (15552000sec) -if (config.url.startsWith('https') && !config.disableHsts) { - app.use(async (ctx, next) => { - ctx.set('strict-transport-security', 'max-age=15552000; preload'); - await next(); - }); -} - -app.use(mount('/api', apiServer)); -app.use(mount('/files', fileServer)); -app.use(mount('/proxy', proxyServer)); - -// Init router -const router = new Router(); - -// Routing -router.use(activityPub.routes()); -router.use(nodeinfo.routes()); -router.use(wellKnown.routes()); - -router.get('/avatar/@:acct', async ctx => { - const { username, host } = Acct.parse(ctx.params.acct); - const user = await Users.findOne({ - where: { - usernameLower: username.toLowerCase(), - host: (host == null) || (host === config.host) ? IsNull() : host, - isSuspended: false, - }, - relations: ['avatar'], - }); - - if (user) { - ctx.redirect(Users.getAvatarUrlSync(user)); - } else { - ctx.redirect('/static-assets/user-unknown.png'); - } -}); - -router.get('/identicon/:x', async ctx => { - const [temp, cleanup] = await createTemp(); - await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); - ctx.set('Content-Type', 'image/png'); - ctx.body = fs.createReadStream(temp).on('close', () => cleanup()); -}); - -router.get('/verify-email/:code', async ctx => { - const profile = await UserProfiles.findOneBy({ - emailVerifyCode: ctx.params.code, - }); - - if (profile != null) { - ctx.body = 'Verify succeeded!'; - ctx.status = 200; - - await UserProfiles.update({ userId: profile.userId }, { - emailVerified: true, - emailVerifyCode: null, - }); - - publishMainStream(profile.userId, 'meUpdated', await Users.pack(profile.userId, { id: profile.userId }, { - detail: true, - includeSecrets: true, - })); - } else { - ctx.status = 404; - } -}); - -// Register router -app.use(router.routes()); - -app.use(mount(webServer)); - -function createServer() { - return http.createServer(app.callback()); -} - -// For testing -export const startServer = () => { - const server = createServer(); - - initializeStreamingServer(server); - - server.listen(config.port); - - return server; -}; - -export default () => new Promise(resolve => { - const server = createServer(); - - initializeStreamingServer(server); - - server.on('error', e => { - switch ((e as any).code) { - case 'EACCES': - serverLogger.error(`You do not have permission to listen on port ${config.port}.`); - break; - case 'EADDRINUSE': - serverLogger.error(`Port ${config.port} is already in use by another process.`); - break; - default: - serverLogger.error(e); - break; - } - - if (cluster.isWorker) { - process.send!('listenFailed'); - } else { - // disableClustering - process.exit(1); - } - }); - - server.listen(config.port, resolve); -}); diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts deleted file mode 100644 index f139d203d2..0000000000 --- a/packages/backend/src/server/nodeinfo.ts +++ /dev/null @@ -1,104 +0,0 @@ -import Router from '@koa/router'; -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, Notes } from '@/models/index.js'; -import { IsNull, MoreThan } from 'typeorm'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; - -const router = new Router(); - -const nodeinfo2_1path = '/nodeinfo/2.1'; -const nodeinfo2_0path = '/nodeinfo/2.0'; - -export const links = [/* (awaiting release) { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: config.url + nodeinfo2_1path -}, */{ - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: config.url + nodeinfo2_0path, -}]; - -const nodeinfo2 = async () => { - const now = Date.now(); - const [ - meta, - total, - activeHalfyear, - activeMonth, - localPosts, - ] = await Promise.all([ - fetchMeta(true), - Users.count({ where: { host: IsNull() } }), - Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }), - Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }), - Notes.count({ where: { userHost: IsNull() } }), - ]); - - const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null; - - return { - software: { - name: 'misskey', - version: config.version, - repository: meta.repositoryUrl, - }, - protocols: ['activitypub'], - services: { - inbound: [] as string[], - outbound: ['atom1.0', 'rss2.0'], - }, - openRegistrations: !meta.disableRegistration, - usage: { - users: { total, activeHalfyear, activeMonth }, - localPosts, - localComments: 0, - }, - metadata: { - nodeName: meta.name, - nodeDescription: meta.description, - maintainer: { - name: meta.maintainerName, - email: meta.maintainerEmail, - }, - langs: meta.langs, - tosUrl: meta.ToSUrl, - repositoryUrl: meta.repositoryUrl, - feedbackUrl: meta.feedbackUrl, - disableRegistration: meta.disableRegistration, - disableLocalTimeline: meta.disableLocalTimeline, - disableGlobalTimeline: meta.disableGlobalTimeline, - emailRequiredForSignup: meta.emailRequiredForSignup, - enableHcaptcha: meta.enableHcaptcha, - enableRecaptcha: meta.enableRecaptcha, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - enableTwitterIntegration: meta.enableTwitterIntegration, - enableGithubIntegration: meta.enableGithubIntegration, - enableDiscordIntegration: meta.enableDiscordIntegration, - enableEmail: meta.enableEmail, - enableServiceWorker: meta.enableServiceWorker, - proxyAccountName: proxyAccount ? proxyAccount.username : null, - themeColor: meta.themeColor || '#86b300', - }, - }; -}; - -const cache = new Cache>>(1000 * 60 * 10); - -router.get(nodeinfo2_1path, async ctx => { - const base = await cache.fetch(null, () => nodeinfo2()); - - ctx.body = { version: '2.1', ...base }; - ctx.set('Cache-Control', 'public, max-age=600'); -}); - -router.get(nodeinfo2_0path, async ctx => { - const base = await cache.fetch(null, () => nodeinfo2()); - - delete base.software.repository; - - ctx.body = { version: '2.0', ...base }; - ctx.set('Cache-Control', 'public, max-age=600'); -}); - -export default router; diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts deleted file mode 100644 index 506ba10ef1..0000000000 --- a/packages/backend/src/server/proxy/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Media Proxy - */ - -import Koa from 'koa'; -import cors from '@koa/cors'; -import Router from '@koa/router'; -import { proxyMedia } from './proxy-media.js'; - -// Init app -const app = new Koa(); -app.use(cors()); -app.use(async (ctx, next) => { - ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); - await next(); -}); - -// Init router -const router = new Router(); - -router.get('/:url*', proxyMedia); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts deleted file mode 100644 index ca036e8fdf..0000000000 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as fs from 'node:fs'; -import Koa from 'koa'; -import sharp from 'sharp'; -import { IImage, convertToWebp } from '@/services/drive/image-processor.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { detectType } from '@/misc/get-file-info.js'; -import { StatusError } from '@/misc/fetch.js'; -import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; -import { serverLogger } from '../index.js'; -import { isMimeImage } from '@/misc/is-mime-image.js'; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export async function proxyMedia(ctx: Koa.Context) { - const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; - - if (typeof url !== 'string') { - ctx.status = 400; - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - try { - await downloadUrl(url, path); - - const { mime, ext } = await detectType(path); - const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); - - let image: IImage; - - if ('static' in ctx.query && isConvertibleImage) { - image = await convertToWebp(path, 498, 280); - } else if ('preview' in ctx.query && isConvertibleImage) { - image = await convertToWebp(path, 200, 200); - } else if ('badge' in ctx.query) { - if (!isConvertibleImage) { - // 画像でないなら404でお茶を濁す - throw new StatusError('Unexpected mime', 404); - } - - const mask = sharp(path) - .resize(96, 96, { - fit: 'inside', - withoutEnlargement: false, - }) - .greyscale() - .normalise() - .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast - .flatten({ background: '#000' }) - .toColorspace('b-w'); - - const stats = await mask.clone().stats(); - - if (stats.entropy < 0.1) { - // エントロピーがあまりない場合は404にする - throw new StatusError('Skip to provide badge', 404); - } - - const data = sharp({ - create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, - }) - .pipelineColorspace('b-w') - .boolean(await mask.png().toBuffer(), 'eor'); - - image = { - data: await data.png().toBuffer(), - ext: 'png', - type: 'image/png', - }; - } else if (mime === 'image/svg+xml') { - image = await convertToWebp(path, 2048, 2048, 1); - } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { - throw new StatusError('Rejected type', 403, 'Rejected type'); - } else { - image = { - data: fs.readFileSync(path), - ext, - type: mime, - }; - } - - ctx.set('Content-Type', image.type); - ctx.set('Cache-Control', 'max-age=31536000, immutable'); - ctx.body = image.data; - } catch (e) { - serverLogger.error(`${e}`); - - if (e instanceof StatusError && (e.statusCode === 302 || e.isClientError)) { - ctx.status = e.statusCode; - } else { - ctx.status = 500; - } - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts new file mode 100644 index 0000000000..67a7efaa25 --- /dev/null +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -0,0 +1,594 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { PathOrFileDescriptor, readFileSync } from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import Koa from 'koa'; +import Router from '@koa/router'; +import send from 'koa-send'; +import favicon from 'koa-favicon'; +import views from 'koa-views'; +import sharp from 'sharp'; +import { createBullBoard } from '@bull-board/api'; +import { BullAdapter } from '@bull-board/api/bullAdapter.js'; +import { KoaAdapter } from '@bull-board/koa'; +import { In, IsNull } from 'typeorm'; +import { Config } from '@/config.js'; +import { getNoteSummary } from '@/misc/get-note-summary.js'; +import { DI } from '@/di-symbols.js'; +import * as Acct from '@/misc/acct.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/queue/QueueModule.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import manifest from './manifest.json' assert { type: 'json' }; +import { FeedService } from './FeedService.js'; +import { UrlPreviewService } from './UrlPreviewService.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const staticAssets = `${_dirname}/../../../assets/`; +const clientAssets = `${_dirname}/../../../../client/assets/`; +const assets = `${_dirname}/../../../../../built/_client_dist_/`; +const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; + +@Injectable() +export class ClientServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private pageEntityService: PageEntityService, + private galleryPostEntityService: GalleryPostEntityService, + private clipEntityService: ClipEntityService, + private channelEntityService: ChannelEntityService, + private metaService: MetaService, + private urlPreviewService: UrlPreviewService, + private feedService: FeedService, + + @Inject('queue:system') public systemQueue: SystemQueue, + @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:deliver') public deliverQueue: DeliverQueue, + @Inject('queue:inbox') public inboxQueue: InboxQueue, + @Inject('queue:db') public dbQueue: DbQueue, + @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + ) { + } + + async #manifestHandler(ctx: Koa.Context) { + // TODO + //const res = structuredClone(manifest); + const res = JSON.parse(JSON.stringify(manifest)); + + const instance = await this.metaService.fetch(true); + + res.short_name = instance.name ?? 'Misskey'; + res.name = instance.name ?? 'Misskey'; + if (instance.themeColor) res.theme_color = instance.themeColor; + + ctx.set('Cache-Control', 'max-age=300'); + ctx.body = res; + } + + public createApp() { + const app = new Koa(); + + //#region Bull Dashboard + const bullBoardPath = '/queue'; + + // Authenticate + app.use(async (ctx, next) => { + if (ctx.path === bullBoardPath || ctx.path.startsWith(bullBoardPath + '/')) { + const token = ctx.cookies.get('token'); + if (token == null) { + ctx.status = 401; + return; + } + const user = await this.usersRepository.findOneBy({ token }); + if (user == null || !(user.isAdmin || user.isModerator)) { + ctx.status = 403; + return; + } + } + await next(); + }); + + const serverAdapter = new KoaAdapter(); + + createBullBoard({ + queues: [ + this.systemQueue, + this.endedPollNotificationQueue, + this.deliverQueue, + this.inboxQueue, + this.dbQueue, + this.objectStorageQueue, + this.webhookDeliverQueue, + ].map(q => new BullAdapter(q)), + serverAdapter, + }); + + serverAdapter.setBasePath(bullBoardPath); + app.use(serverAdapter.registerPlugin()); + //#endregion + + // Init renderer + app.use(views(_dirname + '/views', { + extension: 'pug', + options: { + version: this.config.version, + getClientEntry: () => process.env.NODE_ENV === 'production' ? + this.config.clientEntry : + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], + config: this.config, + }, + })); + + // Serve favicon + app.use(favicon(`${_dirname}/../../../assets/favicon.ico`)); + + // Common request handler + app.use(async (ctx, next) => { + // IFrameの中に入れられないようにする + ctx.set('X-Frame-Options', 'DENY'); + await next(); + }); + + // Init router + const router = new Router(); + + //#region static assets + + router.get('/static-assets/(.*)', async ctx => { + await send(ctx as any, ctx.path.replace('/static-assets/', ''), { + root: staticAssets, + maxage: ms('7 days'), + }); + }); + + router.get('/client-assets/(.*)', async ctx => { + await send(ctx as any, ctx.path.replace('/client-assets/', ''), { + root: clientAssets, + maxage: ms('7 days'), + }); + }); + + router.get('/assets/(.*)', async ctx => { + await send(ctx as any, ctx.path.replace('/assets/', ''), { + root: assets, + maxage: ms('7 days'), + }); + }); + + // Apple touch icon + router.get('/apple-touch-icon.png', async ctx => { + await send(ctx as any, '/apple-touch-icon.png', { + root: staticAssets, + }); + }); + + router.get('/twemoji/(.*)', async ctx => { + const path = ctx.path.replace('/twemoji/', ''); + + if (!path.match(/^[0-9a-f-]+\.svg$/)) { + ctx.status = 404; + return; + } + + ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + + await send(ctx as any, path, { + root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, + maxage: ms('30 days'), + }); + }); + + router.get('/twemoji-badge/(.*)', async ctx => { + const path = ctx.path.replace('/twemoji-badge/', ''); + + if (!path.match(/^[0-9a-f-]+\.png$/)) { + ctx.status = 404; + return; + } + + const mask = await sharp( + `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`, + { density: 1000 }, + ) + .resize(488, 488) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .extend({ + top: 12, + bottom: 12, + left: 12, + right: 12, + background: '#000', + }) + .toColorspace('b-w') + .png() + .toBuffer(); + + const buffer = await sharp({ + create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(mask, 'eor') + .resize(96, 96) + .png() + .toBuffer(); + + ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + ctx.set('Cache-Control', 'max-age=2592000'); + ctx.set('Content-Type', 'image/png'); + ctx.body = buffer; + }); + + // ServiceWorker + router.get('/sw.js', async ctx => { + await send(ctx as any, '/sw.js', { + root: swAssets, + maxage: ms('10 minutes'), + }); + }); + + // Manifest + router.get('/manifest.json', ctx => this.#manifestHandler(ctx)); + + router.get('/robots.txt', async ctx => { + await send(ctx as any, '/robots.txt', { + root: staticAssets, + }); + }); + + //#endregion + + // Docs + router.get('/api-doc', async ctx => { + await send(ctx as any, '/redoc.html', { + root: staticAssets, + }); + }); + + // URL preview endpoint + router.get('/url', ctx => this.urlPreviewService.handle(ctx)); + + router.get('/api.json', async ctx => { + ctx.body = genOpenapiSpec(); + }); + + const getFeed = async (acct: string) => { + const { username, host } = Acct.parse(acct); + const user = await this.usersRepository.findOneBy({ + usernameLower: username.toLowerCase(), + host: host ?? IsNull(), + isSuspended: false, + }); + + return user && await this.feedService.packFeed(user); + }; + + // Atom + router.get('/@:user.atom', async ctx => { + const feed = await getFeed(ctx.params.user); + + if (feed) { + ctx.set('Content-Type', 'application/atom+xml; charset=utf-8'); + ctx.body = feed.atom1(); + } else { + ctx.status = 404; + } + }); + + // RSS + router.get('/@:user.rss', async ctx => { + const feed = await getFeed(ctx.params.user); + + if (feed) { + ctx.set('Content-Type', 'application/rss+xml; charset=utf-8'); + ctx.body = feed.rss2(); + } else { + ctx.status = 404; + } + }); + + // JSON + router.get('/@:user.json', async ctx => { + const feed = await getFeed(ctx.params.user); + + if (feed) { + ctx.set('Content-Type', 'application/json; charset=utf-8'); + ctx.body = feed.json1(); + } else { + ctx.status = 404; + } + }); + + //#region SSR (for crawlers) + // User + router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { + const { username, host } = Acct.parse(ctx.params.user); + const user = await this.usersRepository.findOneBy({ + usernameLower: username.toLowerCase(), + host: host ?? IsNull(), + isSuspended: false, + }); + + if (user != null) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + const meta = await this.metaService.fetch(); + const me = profile.fields + ? profile.fields + .filter(filed => filed.value != null && filed.value.match(/^https?:/)) + .map(field => field.value) + : []; + + await ctx.render('user', { + user, profile, me, + avatarUrl: await this.userEntityService.getAvatarUrl(user), + sub: ctx.params.sub, + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + ctx.set('Cache-Control', 'public, max-age=15'); + } else { + // リモートユーザーなので + // モデレータがAPI経由で参照可能にするために404にはしない + await next(); + } + }); + + router.get('/users/:user', async ctx => { + const user = await this.usersRepository.findOneBy({ + id: ctx.params.user, + host: IsNull(), + isSuspended: false, + }); + + if (user == null) { + ctx.status = 404; + return; + } + + ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); + }); + + // Note + router.get('/notes/:note', async (ctx, next) => { + const note = await this.notesRepository.findOneBy({ + id: ctx.params.note, + visibility: In(['public', 'home']), + }); + + if (note) { + const _note = await this.noteEntityService.pack(note); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); + const meta = await this.metaService.fetch(); + await ctx.render('note', { + note: _note, + profile, + avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: note.userId })), + // TODO: Let locale changeable by instance setting + summary: getNoteSummary(_note), + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + + ctx.set('Cache-Control', 'public, max-age=15'); + + return; + } + + await next(); + }); + + // Page + router.get('/@:user/pages/:page', async (ctx, next) => { + const { username, host } = Acct.parse(ctx.params.user); + const user = await this.usersRepository.findOneBy({ + usernameLower: username.toLowerCase(), + host: host ?? IsNull(), + }); + + if (user == null) return; + + const page = await this.pagesRepository.findOneBy({ + name: ctx.params.page, + userId: user.id, + }); + + if (page) { + const _page = await this.pageEntityService.pack(page); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); + const meta = await this.metaService.fetch(); + await ctx.render('page', { + page: _page, + profile, + avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: page.userId })), + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + + if (['public'].includes(page.visibility)) { + ctx.set('Cache-Control', 'public, max-age=15'); + } else { + ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); + } + + return; + } + + await next(); + }); + + // Clip + // TODO: 非publicなclipのハンドリング + router.get('/clips/:clip', async (ctx, next) => { + const clip = await this.clipsRepository.findOneBy({ + id: ctx.params.clip, + }); + + if (clip) { + const _clip = await this.clipEntityService.pack(clip); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); + const meta = await this.metaService.fetch(); + await ctx.render('clip', { + clip: _clip, + profile, + avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: clip.userId })), + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + + ctx.set('Cache-Control', 'public, max-age=15'); + + return; + } + + await next(); + }); + + // Gallery post + router.get('/gallery/:post', async (ctx, next) => { + const post = await this.galleryPostsRepository.findOneBy({ id: ctx.params.post }); + + if (post) { + const _post = await this.galleryPostEntityService.pack(post); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); + const meta = await this.metaService.fetch(); + await ctx.render('gallery-post', { + post: _post, + profile, + avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: post.userId })), + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + + ctx.set('Cache-Control', 'public, max-age=15'); + + return; + } + + await next(); + }); + + // Channel + router.get('/channels/:channel', async (ctx, next) => { + const channel = await this.channelsRepository.findOneBy({ + id: ctx.params.channel, + }); + + if (channel) { + const _channel = await this.channelEntityService.pack(channel); + const meta = await this.metaService.fetch(); + await ctx.render('channel', { + channel: _channel, + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + + ctx.set('Cache-Control', 'public, max-age=15'); + + return; + } + + await next(); + }); + //#endregion + + router.get('/_info_card_', async ctx => { + const meta = await this.metaService.fetch(true); + + ctx.remove('X-Frame-Options'); + + await ctx.render('info-card', { + version: this.config.version, + host: this.config.host, + meta: meta, + originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), + originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), + }); + }); + + router.get('/bios', async ctx => { + await ctx.render('bios', { + version: this.config.version, + }); + }); + + router.get('/cli', async ctx => { + await ctx.render('cli', { + version: this.config.version, + }); + }); + + const override = (source: string, target: string, depth = 0) => + [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); + + router.get('/flush', async ctx => { + await ctx.render('flush'); + }); + + // streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる + router.get('/streaming', async ctx => { + ctx.status = 503; + ctx.set('Cache-Control', 'private, max-age=0'); + }); + + // Render base html for all requests + router.get('(.*)', async ctx => { + const meta = await this.metaService.fetch(); + await ctx.render('base', { + img: meta.bannerUrl, + title: meta.name ?? 'Misskey', + instanceName: meta.name ?? 'Misskey', + desc: meta.description, + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + ctx.set('Cache-Control', 'public, max-age=15'); + }); + + // Register router + app.use(router.routes()); + + return app; + } +} diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts new file mode 100644 index 0000000000..8b676aebe5 --- /dev/null +++ b/packages/backend/src/server/web/FeedService.ts @@ -0,0 +1,86 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, IsNull } from 'typeorm'; +import { Feed } from 'feed'; +import { DI } from '@/di-symbols.js'; +import { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import { 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'; + +@Injectable() +export class FeedService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + ) { + } + + public async packFeed(user: User) { + const author = { + link: `${this.config.url}/@${user.username}`, + name: user.name ?? user.username, + }; + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + renoteId: IsNull(), + visibility: In(['public', 'home']), + }, + order: { createdAt: -1 }, + take: 20, + }); + + const feed = new Feed({ + id: author.link, + title: `${author.name} (@${user.username}@${this.config.host})`, + updated: notes[0].createdAt, + generator: 'Misskey', + description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, + link: author.link, + image: await this.userEntityService.getAvatarUrl(user), + feedLinks: { + json: `${author.link}.json`, + atom: `${author.link}.atom`, + }, + author, + copyright: user.name ?? user.username, + }); + + for (const note of notes) { + const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({ + id: In(note.fileIds), + }) : []; + const file = files.find(file => file.type.startsWith('image/')); + + feed.addItem({ + title: `New note by ${author.name}`, + link: `${this.config.url}/notes/${note.id}`, + date: note.createdAt, + description: note.cw ?? undefined, + content: note.text ?? undefined, + image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined, + }); + } + + return feed; + } +} diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts new file mode 100644 index 0000000000..4e3b456144 --- /dev/null +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -0,0 +1,84 @@ +import { Inject, Injectable } from '@nestjs/common'; +import summaly from 'summaly'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { MetaService } from '@/core/MetaService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import Logger from '@/logger.js'; +import { query } from '@/misc/prelude/url.js'; +import type Koa from 'koa'; + +@Injectable() +export class UrlPreviewService { + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private metaService: MetaService, + private httpRequestService: HttpRequestService, + ) { + this.#logger = new Logger('url-preview'); + } + + #wrap(url?: string): string | null { + return url != null + ? url.match(/^https?:\/\//) + ? `${this.config.url}/proxy/preview.webp?${query({ + url, + preview: '1', + })}` + : url + : null; + } + + public async handle(ctx: Koa.Context) { + const url = ctx.query.url; + if (typeof url !== 'string') { + ctx.status = 400; + return; + } + + const lang = ctx.query.lang; + if (Array.isArray(lang)) { + ctx.status = 400; + return; + } + + const meta = await this.metaService.fetch(); + + this.#logger.info(meta.summalyProxy + ? `(Proxy) Getting preview of ${url}@${lang} ...` + : `Getting preview of ${url}@${lang} ...`); + + try { + const summary = meta.summalyProxy ? await this.httpRequestService.getJson(`${meta.summalyProxy}?${query({ + url: url, + lang: lang ?? 'ja-JP', + })}`) : await summaly.default(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + }); + + this.#logger.succ(`Got preview of ${url}: ${summary.title}`); + + summary.icon = this.#wrap(summary.icon); + summary.thumbnail = this.#wrap(summary.thumbnail); + + // Cache 7days + ctx.set('Cache-Control', 'max-age=604800, immutable'); + + ctx.body = summary; + } catch (err) { + this.#logger.warn(`Failed to get preview of ${url}: ${err}`); + ctx.status = 200; + ctx.set('Cache-Control', 'max-age=86400, immutable'); + ctx.body = '{}'; + } + } +} diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts deleted file mode 100644 index 4abe2885cf..0000000000 --- a/packages/backend/src/server/web/feed.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Feed } from 'feed'; -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js'; - -export default async function(user: User) { - const author = { - link: `${config.url}/@${user.username}`, - name: user.name || user.username, - }; - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - const notes = await Notes.find({ - where: { - userId: user.id, - renoteId: IsNull(), - visibility: In(['public', 'home']), - }, - order: { createdAt: -1 }, - take: 20, - }); - - const feed = new Feed({ - id: author.link, - title: `${author.name} (@${user.username}@${config.host})`, - updated: notes[0].createdAt, - generator: 'Misskey', - description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, - link: author.link, - image: await Users.getAvatarUrl(user), - feedLinks: { - json: `${author.link}.json`, - atom: `${author.link}.atom`, - }, - author, - copyright: user.name || user.username, - }); - - for (const note of notes) { - const files = note.fileIds.length > 0 ? await DriveFiles.findBy({ - id: In(note.fileIds), - }) : []; - const file = files.find(file => file.type.startsWith('image/')); - - feed.addItem({ - title: `New note by ${author.name}`, - link: `${config.url}/notes/${note.id}`, - date: note.createdAt, - description: note.cw || undefined, - content: note.text || undefined, - image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined, - }); - } - - return feed; -} diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts deleted file mode 100644 index be95becb68..0000000000 --- a/packages/backend/src/server/web/index.ts +++ /dev/null @@ -1,521 +0,0 @@ -/** - * Web Client Server - */ - -import { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { PathOrFileDescriptor, readFileSync } from 'node:fs'; -import ms from 'ms'; -import Koa from 'koa'; -import Router from '@koa/router'; -import send from 'koa-send'; -import favicon from 'koa-favicon'; -import views from 'koa-views'; -import sharp from 'sharp'; -import { createBullBoard } from '@bull-board/api'; -import { BullAdapter } from '@bull-board/api/bullAdapter.js'; -import { KoaAdapter } from '@bull-board/koa'; - -import { In, IsNull } from 'typeorm'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import config from '@/config/index.js'; -import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js'; -import * as Acct from '@/misc/acct.js'; -import { getNoteSummary } from '@/misc/get-note-summary.js'; -import { queues } from '@/queue/queues.js'; -import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; -import { urlPreviewHandler } from './url-preview.js'; -import { manifestHandler } from './manifest.js'; -import packFeed from './feed.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const staticAssets = `${_dirname}/../../../assets/`; -const clientAssets = `${_dirname}/../../../../client/assets/`; -const assets = `${_dirname}/../../../../../built/_client_dist_/`; -const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; - -// Init app -const app = new Koa(); - -//#region Bull Dashboard -const bullBoardPath = '/queue'; - -// Authenticate -app.use(async (ctx, next) => { - if (ctx.path === bullBoardPath || ctx.path.startsWith(bullBoardPath + '/')) { - const token = ctx.cookies.get('token'); - if (token == null) { - ctx.status = 401; - return; - } - const user = await Users.findOneBy({ token }); - if (user == null || !(user.isAdmin || user.isModerator)) { - ctx.status = 403; - return; - } - } - await next(); -}); - -const serverAdapter = new KoaAdapter(); - -createBullBoard({ - queues: queues.map(q => new BullAdapter(q)), - serverAdapter, -}); - -serverAdapter.setBasePath(bullBoardPath); -app.use(serverAdapter.registerPlugin()); -//#endregion - -// Init renderer -app.use(views(_dirname + '/views', { - extension: 'pug', - options: { - version: config.version, - getClientEntry: () => process.env.NODE_ENV === 'production' ? - config.clientEntry : - JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], - config, - }, -})); - -// Serve favicon -app.use(favicon(`${_dirname}/../../../assets/favicon.ico`)); - -// Common request handler -app.use(async (ctx, next) => { - // IFrameの中に入れられないようにする - ctx.set('X-Frame-Options', 'DENY'); - await next(); -}); - -// Init router -const router = new Router(); - -//#region static assets - -router.get('/static-assets/(.*)', async ctx => { - await send(ctx as any, ctx.path.replace('/static-assets/', ''), { - root: staticAssets, - maxage: ms('7 days'), - }); -}); - -router.get('/client-assets/(.*)', async ctx => { - await send(ctx as any, ctx.path.replace('/client-assets/', ''), { - root: clientAssets, - maxage: ms('7 days'), - }); -}); - -router.get('/assets/(.*)', async ctx => { - await send(ctx as any, ctx.path.replace('/assets/', ''), { - root: assets, - maxage: ms('7 days'), - }); -}); - -// Apple touch icon -router.get('/apple-touch-icon.png', async ctx => { - await send(ctx as any, '/apple-touch-icon.png', { - root: staticAssets, - }); -}); - -router.get('/twemoji/(.*)', async ctx => { - const path = ctx.path.replace('/twemoji/', ''); - - if (!path.match(/^[0-9a-f-]+\.svg$/)) { - ctx.status = 404; - return; - } - - ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - - await send(ctx as any, path, { - root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, - maxage: ms('30 days'), - }); -}); - -router.get('/twemoji-badge/(.*)', async ctx => { - const path = ctx.path.replace('/twemoji-badge/', ''); - - if (!path.match(/^[0-9a-f-]+\.png$/)) { - ctx.status = 404; - return; - } - - const mask = await sharp( - `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`, - { density: 1000 }, - ) - .resize(488, 488) - .greyscale() - .normalise() - .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast - .flatten({ background: '#000' }) - .extend({ - top: 12, - bottom: 12, - left: 12, - right: 12, - background: '#000', - }) - .toColorspace('b-w') - .png() - .toBuffer(); - - const buffer = await sharp({ - create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, - }) - .pipelineColorspace('b-w') - .boolean(mask, 'eor') - .resize(96, 96) - .png() - .toBuffer(); - - ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - ctx.set('Cache-Control', 'max-age=2592000'); - ctx.set('Content-Type', 'image/png'); - ctx.body = buffer; -}); - -// ServiceWorker -router.get(`/sw.js`, async ctx => { - await send(ctx as any, `/sw.js`, { - root: swAssets, - maxage: ms('10 minutes'), - }); -}); - -// Manifest -router.get('/manifest.json', manifestHandler); - -router.get('/robots.txt', async ctx => { - await send(ctx as any, '/robots.txt', { - root: staticAssets, - }); -}); - -//#endregion - -// Docs -router.get('/api-doc', async ctx => { - await send(ctx as any, '/redoc.html', { - root: staticAssets, - }); -}); - -// URL preview endpoint -router.get('/url', urlPreviewHandler); - -router.get('/api.json', async ctx => { - ctx.body = genOpenapiSpec(); -}); - -const getFeed = async (acct: string) => { - const { username, host } = Acct.parse(acct); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - return user && await packFeed(user); -}; - -// Atom -router.get('/@:user.atom', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/atom+xml; charset=utf-8'); - ctx.body = feed.atom1(); - } else { - ctx.status = 404; - } -}); - -// RSS -router.get('/@:user.rss', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/rss+xml; charset=utf-8'); - ctx.body = feed.rss2(); - } else { - ctx.status = 404; - } -}); - -// JSON -router.get('/@:user.json', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/json; charset=utf-8'); - ctx.body = feed.json1(); - } else { - ctx.status = 404; - } -}); - -//#region SSR (for crawlers) -// User -router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - if (user != null) { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); - const me = profile.fields - ? profile.fields - .filter(filed => filed.value != null && filed.value.match(/^https?:/)) - .map(field => field.value) - : []; - - await ctx.render('user', { - user, profile, me, - avatarUrl: await Users.getAvatarUrl(user), - sub: ctx.params.sub, - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - ctx.set('Cache-Control', 'public, max-age=15'); - } else { - // リモートユーザーなので - // モデレータがAPI経由で参照可能にするために404にはしない - await next(); - } -}); - -router.get('/users/:user', async ctx => { - const user = await Users.findOneBy({ - id: ctx.params.user, - host: IsNull(), - isSuspended: false, - }); - - if (user == null) { - ctx.status = 404; - return; - } - - ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); -}); - -// Note -router.get('/notes/:note', async (ctx, next) => { - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(['public', 'home']), - }); - - if (note) { - const _note = await Notes.pack(note); - const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); - await ctx.render('note', { - note: _note, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })), - // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); - -// Page -router.get('/@:user/pages/:page', async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - }); - - if (user == null) return; - - const page = await Pages.findOneBy({ - name: ctx.params.page, - userId: user.id, - }); - - if (page) { - const _page = await Pages.pack(page); - const profile = await UserProfiles.findOneByOrFail({ userId: page.userId }); - const meta = await fetchMeta(); - await ctx.render('page', { - page: _page, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: page.userId })), - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - if (['public'].includes(page.visibility)) { - ctx.set('Cache-Control', 'public, max-age=15'); - } else { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - } - - return; - } - - await next(); -}); - -// Clip -// TODO: 非publicなclipのハンドリング -router.get('/clips/:clip', async (ctx, next) => { - const clip = await Clips.findOneBy({ - id: ctx.params.clip, - }); - - if (clip) { - const _clip = await Clips.pack(clip); - const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId }); - const meta = await fetchMeta(); - await ctx.render('clip', { - clip: _clip, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: clip.userId })), - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); - -// Gallery post -router.get('/gallery/:post', async (ctx, next) => { - const post = await GalleryPosts.findOneBy({ id: ctx.params.post }); - - if (post) { - const _post = await GalleryPosts.pack(post); - const profile = await UserProfiles.findOneByOrFail({ userId: post.userId }); - const meta = await fetchMeta(); - await ctx.render('gallery-post', { - post: _post, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: post.userId })), - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); - -// Channel -router.get('/channels/:channel', async (ctx, next) => { - const channel = await Channels.findOneBy({ - id: ctx.params.channel, - }); - - if (channel) { - const _channel = await Channels.pack(channel); - const meta = await fetchMeta(); - await ctx.render('channel', { - channel: _channel, - instanceName: meta.name || 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); -//#endregion - -router.get('/_info_card_', async ctx => { - const meta = await fetchMeta(true); - - ctx.remove('X-Frame-Options'); - - await ctx.render('info-card', { - version: config.version, - host: config.host, - meta: meta, - originalUsersCount: await Users.countBy({ host: IsNull() }), - originalNotesCount: await Notes.countBy({ userHost: IsNull() }), - }); -}); - -router.get('/bios', async ctx => { - await ctx.render('bios', { - version: config.version, - }); -}); - -router.get('/cli', async ctx => { - await ctx.render('cli', { - version: config.version, - }); -}); - -const override = (source: string, target: string, depth = 0) => - [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); - -router.get('/flush', async ctx => { - await ctx.render('flush'); -}); - -// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる -router.get('/streaming', async ctx => { - ctx.status = 503; - ctx.set('Cache-Control', 'private, max-age=0'); -}); - -// Render base html for all requests -router.get('(.*)', async ctx => { - const meta = await fetchMeta(); - await ctx.render('base', { - img: meta.bannerUrl, - title: meta.name || 'Misskey', - instanceName: meta.name || 'Misskey', - desc: meta.description, - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - ctx.set('Cache-Control', 'public, max-age=15'); -}); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts deleted file mode 100644 index ee568b8077..0000000000 --- a/packages/backend/src/server/web/manifest.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Koa from 'koa'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import manifest from './manifest.json' assert { type: 'json' }; - -export const manifestHandler = async (ctx: Koa.Context) => { - // TODO - //const res = structuredClone(manifest); - const res = JSON.parse(JSON.stringify(manifest)); - - const instance = await fetchMeta(true); - - res.short_name = instance.name || 'Misskey'; - res.name = instance.name || 'Misskey'; - if (instance.themeColor) res.theme_color = instance.themeColor; - - ctx.set('Cache-Control', 'max-age=300'); - ctx.body = res; -}; diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts deleted file mode 100644 index 1e259649f9..0000000000 --- a/packages/backend/src/server/web/url-preview.ts +++ /dev/null @@ -1,65 +0,0 @@ -import Koa from 'koa'; -import summaly from 'summaly'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import Logger from '@/services/logger.js'; -import config from '@/config/index.js'; -import { query } from '@/prelude/url.js'; -import { getJson } from '@/misc/fetch.js'; - -const logger = new Logger('url-preview'); - -export const urlPreviewHandler = async (ctx: Koa.Context) => { - const url = ctx.query.url; - if (typeof url !== 'string') { - ctx.status = 400; - return; - } - - const lang = ctx.query.lang; - if (Array.isArray(lang)) { - ctx.status = 400; - return; - } - - const meta = await fetchMeta(); - - logger.info(meta.summalyProxy - ? `(Proxy) Getting preview of ${url}@${lang} ...` - : `Getting preview of ${url}@${lang} ...`); - - try { - const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({ - url: url, - lang: lang ?? 'ja-JP', - })}`) : await summaly.default(url, { - followRedirects: false, - lang: lang ?? 'ja-JP', - }); - - logger.succ(`Got preview of ${url}: ${summary.title}`); - - summary.icon = wrap(summary.icon); - summary.thumbnail = wrap(summary.thumbnail); - - // Cache 7days - ctx.set('Cache-Control', 'max-age=604800, immutable'); - - ctx.body = summary; - } catch (err) { - logger.warn(`Failed to get preview of ${url}: ${err}`); - ctx.status = 200; - ctx.set('Cache-Control', 'max-age=86400, immutable'); - ctx.body = '{}'; - } -}; - -function wrap(url?: string): string | null { - return url != null - ? url.match(/^https?:\/\//) - ? `${config.url}/proxy/preview.webp?${query({ - url, - preview: '1', - })}` - : url - : null; -} diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts deleted file mode 100644 index 1d094f2edd..0000000000 --- a/packages/backend/src/server/well-known.ts +++ /dev/null @@ -1,151 +0,0 @@ -import Router from '@koa/router'; - -import config from '@/config/index.js'; -import * as Acct from '@/misc/acct.js'; -import { links } from './nodeinfo.js'; -import { escapeAttribute, escapeValue } from '@/prelude/xml.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { FindOptionsWhere, IsNull } from 'typeorm'; - -// Init router -const router = new Router(); - -const XRD = (...x: { element: string, value?: string, attributes?: Record }[]) => - `${x.map(({ element, value, attributes }) => - `<${ - Object.entries(typeof attributes === 'object' && attributes || {}).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element) - }${ - typeof value === 'string' ? `>${escapeValue(value)}`).reduce((a, c) => a + c, '')}`; - -const allPath = '/.well-known/(.*)'; -const webFingerPath = '/.well-known/webfinger'; -const jrd = 'application/jrd+json'; -const xrd = 'application/xrd+xml'; - -router.use(allPath, async (ctx, next) => { - ctx.set({ - 'Access-Control-Allow-Headers': 'Accept', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Expose-Headers': 'Vary', - }); - await next(); -}); - -router.options(allPath, async ctx => { - ctx.status = 204; -}); - -router.get('/.well-known/host-meta', async ctx => { - ctx.set('Content-Type', xrd); - ctx.body = XRD({ element: 'Link', attributes: { - rel: 'lrdd', - type: xrd, - template: `${config.url}${webFingerPath}?resource={uri}`, - } }); -}); - -router.get('/.well-known/host-meta.json', async ctx => { - ctx.set('Content-Type', jrd); - ctx.body = { - links: [{ - rel: 'lrdd', - type: jrd, - template: `${config.url}${webFingerPath}?resource={uri}`, - }], - }; -}); - -router.get('/.well-known/nodeinfo', async ctx => { - ctx.body = { links }; -}); - -/* TODO -router.get('/.well-known/change-password', async ctx => { -}); -*/ - -router.get(webFingerPath, async ctx => { - const fromId = (id: User['id']): FindOptionsWhere => ({ - id, - host: IsNull(), - isSuspended: false, - }); - - const generateQuery = (resource: string): FindOptionsWhere | number => - resource.startsWith(`${config.url.toLowerCase()}/users/`) ? - fromId(resource.split('/').pop()!) : - fromAcct(Acct.parse( - resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop()! : - resource.startsWith('acct:') ? resource.slice('acct:'.length) : - resource)); - - const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => - !acct.host || acct.host === config.host.toLowerCase() ? { - usernameLower: acct.username, - host: IsNull(), - isSuspended: false, - } : 422; - - if (typeof ctx.query.resource !== 'string') { - ctx.status = 400; - return; - } - - const query = generateQuery(ctx.query.resource.toLowerCase()); - - if (typeof query === 'number') { - ctx.status = query; - return; - } - - const user = await Users.findOneBy(query); - - if (user == null) { - ctx.status = 404; - return; - } - - const subject = `acct:${user.username}@${config.host}`; - const self = { - rel: 'self', - type: 'application/activity+json', - href: `${config.url}/users/${user.id}`, - }; - const profilePage = { - rel: 'http://webfinger.net/rel/profile-page', - type: 'text/html', - href: `${config.url}/@${user.username}`, - }; - const subscribe = { - rel: 'http://ostatus.org/schema/1.0/subscribe', - template: `${config.url}/authorize-follow?acct={uri}`, - }; - - if (ctx.accepts(jrd, xrd) === xrd) { - ctx.body = XRD( - { element: 'Subject', value: subject }, - { element: 'Link', attributes: self }, - { element: 'Link', attributes: profilePage }, - { element: 'Link', attributes: subscribe }); - ctx.type = xrd; - } else { - ctx.body = { - subject, - links: [self, profilePage, subscribe], - }; - ctx.type = jrd; - } - - ctx.vary('Accept'); - ctx.set('Cache-Control', 'public, max-age=180'); -}); - -// Return 404 for other .well-known -router.all(allPath, async ctx => { - ctx.status = 404; -}); - -export default router; diff --git a/packages/backend/src/services/add-note-to-antenna.ts b/packages/backend/src/services/add-note-to-antenna.ts deleted file mode 100644 index 1f344222e1..0000000000 --- a/packages/backend/src/services/add-note-to-antenna.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Antenna } from '@/models/entities/antenna.js'; -import { Note } from '@/models/entities/note.js'; -import { AntennaNotes, Mutings, Notes } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { publishAntennaStream, publishMainStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; - -export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }) { - // 通知しない設定になっているか、自分自身の投稿なら既読にする - const read = !antenna.notify || (antenna.userId === noteUser.id); - - AntennaNotes.insert({ - id: genId(), - antennaId: antenna.id, - noteId: note.id, - read: read, - }); - - publishAntennaStream(antenna.id, 'note', note); - - if (!read) { - const mutings = await Mutings.find({ - where: { - muterId: antenna.userId, - }, - select: ['muteeId'], - }); - - // Copy - const _note: Note = { - ...note, - }; - - if (note.replyId != null) { - _note.reply = await Notes.findOneByOrFail({ id: note.replyId }); - } - if (note.renoteId != null) { - _note.renote = await Notes.findOneByOrFail({ id: note.renoteId }); - } - - if (isUserRelated(_note, new Set(mutings.map(x => x.muteeId)))) { - return; - } - - // 2秒経っても既読にならなかったら通知 - setTimeout(async () => { - const unread = await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false }); - if (unread) { - publishMainStream(antenna.userId, 'unreadAntenna', antenna); - } - }, 2000); - } -} diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts deleted file mode 100644 index a2c61cca22..0000000000 --- a/packages/backend/src/services/blocking/create.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { renderBlock } from '@/remote/activitypub/renderer/block.js'; -import { deliver } from '@/queue/index.js'; -import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { Blocking } from '@/models/entities/blocking.js'; -import { User } from '@/models/entities/user.js'; -import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; -import { perUserFollowingChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { webhookDeliver } from '@/queue/index.js'; - -export default async function(blocker: User, blockee: User) { - await Promise.all([ - cancelRequest(blocker, blockee), - cancelRequest(blockee, blocker), - unFollow(blocker, blockee), - unFollow(blockee, blocker), - removeFromList(blockee, blocker), - ]); - - const blocking = { - id: genId(), - createdAt: new Date(), - blocker, - blockerId: blocker.id, - blockee, - blockeeId: blockee.id, - } as Blocking; - - await Blockings.insert(blocking); - - if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderBlock(blocking)); - deliver(blocker, content, blockee.inbox); - } -} - -async function cancelRequest(follower: User, followee: User) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (request == null) { - return; - } - - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (Users.isLocalUser(followee)) { - Users.pack(followee, followee, { - detail: true, - }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); - } - - if (Users.isLocalUser(follower)) { - Users.pack(followee, follower, { - detail: true, - }).then(async packed => { - publishUserEvent(follower.id, 'unfollow', packed); - publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } - }); - } - - // リモートにフォローリクエストをしていたらUndoFollow送信 - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - deliver(follower, content, followee.inbox); - } - - // リモートからフォローリクエストを受けていたらReject送信 - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee)); - deliver(followee, content, follower.inbox); - } -} - -async function unFollow(follower: User, followee: User) { - const following = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (following == null) { - return; - } - - await Promise.all([ - Followings.delete(following.id), - Users.decrement({ id: follower.id }, 'followingCount', 1), - Users.decrement({ id: followee.id }, 'followersCount', 1), - perUserFollowingChart.update(follower, followee, false), - ]); - - // Publish unfollow event - if (Users.isLocalUser(follower)) { - Users.pack(followee, follower, { - detail: true, - }).then(async packed => { - publishUserEvent(follower.id, 'unfollow', packed); - publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } - }); - } - - // リモートにフォローをしていたらUndoFollow送信 - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - deliver(follower, content, followee.inbox); - } -} - -async function removeFromList(listOwner: User, user: User) { - const userLists = await UserLists.findBy({ - userId: listOwner.id, - }); - - for (const userList of userLists) { - await UserListJoinings.delete({ - userListId: userList.id, - userId: user.id, - }); - } -} diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts deleted file mode 100644 index cb16651bc0..0000000000 --- a/packages/backend/src/services/blocking/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { renderBlock } from '@/remote/activitypub/renderer/block.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { deliver } from '@/queue/index.js'; -import Logger from '../logger.js'; -import { CacheableUser, User } from '@/models/entities/user.js'; -import { Blockings, Users } from '@/models/index.js'; - -const logger = new Logger('blocking/delete'); - -export default async function(blocker: CacheableUser, blockee: CacheableUser) { - const blocking = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (blocking == null) { - logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした'); - return; - } - - // Since we already have the blocker and blockee, we do not need to fetch - // them in the query above and can just manually insert them here. - blocking.blocker = blocker; - blocking.blockee = blockee; - - Blockings.delete(blocking.id); - - // deliver if remote bloking - if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderUndo(renderBlock(blocking), blocker)); - deliver(blocker, content, blockee.inbox); - } -} diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts deleted file mode 100644 index d952ea53bd..0000000000 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { name, schema } from './entities/active-users.js'; - -const week = 1000 * 60 * 60 * 24 * 7; -const month = 1000 * 60 * 60 * 24 * 30; -const year = 1000 * 60 * 60 * 24 * 365; - -/** - * アクティブユーザーに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class ActiveUsersChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { - await this.commit({ - 'read': [user.id], - 'registeredWithinWeek': (Date.now() - user.createdAt.getTime() < week) ? [user.id] : [], - 'registeredWithinMonth': (Date.now() - user.createdAt.getTime() < month) ? [user.id] : [], - 'registeredWithinYear': (Date.now() - user.createdAt.getTime() < year) ? [user.id] : [], - 'registeredOutsideWeek': (Date.now() - user.createdAt.getTime() > week) ? [user.id] : [], - 'registeredOutsideMonth': (Date.now() - user.createdAt.getTime() > month) ? [user.id] : [], - 'registeredOutsideYear': (Date.now() - user.createdAt.getTime() > year) ? [user.id] : [], - }); - } - - 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/services/chart/charts/ap-request.ts b/packages/backend/src/services/chart/charts/ap-request.ts deleted file mode 100644 index e9e42ade7f..0000000000 --- a/packages/backend/src/services/chart/charts/ap-request.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/ap-request.js'; - -/** - * Chart about ActivityPub requests - */ -// eslint-disable-next-line import/no-default-export -export default class ApRequestChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async deliverSucc(): Promise { - await this.commit({ - 'deliverSucceeded': 1, - }); - } - - public async deliverFail(): Promise { - await this.commit({ - 'deliverFailed': 1, - }); - } - - public async inbox(): Promise { - await this.commit({ - 'inboxReceived': 1, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/drive.ts b/packages/backend/src/services/chart/charts/drive.ts deleted file mode 100644 index 0eeba90dd3..0000000000 --- a/packages/backend/src/services/chart/charts/drive.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { DriveFiles } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { name, schema } from './entities/drive.js'; - -/** - * ドライブに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class DriveChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(file: DriveFile, isAdditional: boolean): Promise { - const fileSizeKb = file.size / 1000; - await this.commit(file.userHost === null ? { - 'local.incCount': isAdditional ? 1 : 0, - 'local.incSize': isAdditional ? fileSizeKb : 0, - 'local.decCount': isAdditional ? 0 : 1, - 'local.decSize': isAdditional ? 0 : fileSizeKb, - } : { - 'remote.incCount': isAdditional ? 1 : 0, - 'remote.incSize': isAdditional ? fileSizeKb : 0, - 'remote.decCount': isAdditional ? 0 : 1, - 'remote.decSize': isAdditional ? 0 : fileSizeKb, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/entities/active-users.ts b/packages/backend/src/services/chart/charts/entities/active-users.ts deleted file mode 100644 index 5767b76f8e..0000000000 --- a/packages/backend/src/services/chart/charts/entities/active-users.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'activeUsers'; - -export const schema = { - 'readWrite': { intersection: ['read', 'write'], range: 'small' }, - 'read': { uniqueIncrement: true, range: 'small' }, - 'write': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinWeek': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinMonth': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinYear': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideYear': { uniqueIncrement: true, range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/ap-request.ts b/packages/backend/src/services/chart/charts/entities/ap-request.ts deleted file mode 100644 index 3a9f3dacfd..0000000000 --- a/packages/backend/src/services/chart/charts/entities/ap-request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'apRequest'; - -export const schema = { - 'deliverFailed': { }, - 'deliverSucceeded': { }, - 'inboxReceived': { }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/drive.ts b/packages/backend/src/services/chart/charts/entities/drive.ts deleted file mode 100644 index 4bf5bb729e..0000000000 --- a/packages/backend/src/services/chart/charts/entities/drive.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'drive'; - -export const schema = { - 'local.incCount': {}, - 'local.incSize': {}, // in kilobyte - 'local.decCount': {}, - 'local.decSize': {}, // in kilobyte - 'remote.incCount': {}, - 'remote.incSize': {}, // in kilobyte - 'remote.decCount': {}, - 'remote.decSize': {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/federation.ts b/packages/backend/src/services/chart/charts/entities/federation.ts deleted file mode 100644 index a8466b0b4c..0000000000 --- a/packages/backend/src/services/chart/charts/entities/federation.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'federation'; - -export const schema = { - 'deliveredInstances': { uniqueIncrement: true, range: 'small' }, - 'inboxInstances': { uniqueIncrement: true, range: 'small' }, - 'stalled': { uniqueIncrement: true, range: 'small' }, - 'sub': { accumulate: true, range: 'small' }, - 'pub': { accumulate: true, range: 'small' }, - 'pubsub': { accumulate: true, range: 'small' }, - 'subActive': { accumulate: true, range: 'small' }, - 'pubActive': { accumulate: true, range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/hashtag.ts b/packages/backend/src/services/chart/charts/entities/hashtag.ts deleted file mode 100644 index 4d04039047..0000000000 --- a/packages/backend/src/services/chart/charts/entities/hashtag.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'hashtag'; - -export const schema = { - 'local.users': { uniqueIncrement: true }, - 'remote.users': { uniqueIncrement: true }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/instance.ts b/packages/backend/src/services/chart/charts/entities/instance.ts deleted file mode 100644 index 06962120e2..0000000000 --- a/packages/backend/src/services/chart/charts/entities/instance.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'instance'; - -export const schema = { - 'requests.failed': { range: 'small' }, - 'requests.succeeded': { range: 'small' }, - 'requests.received': { range: 'small' }, - 'notes.total': { accumulate: true }, - 'notes.inc': {}, - 'notes.dec': {}, - 'notes.diffs.normal': {}, - 'notes.diffs.reply': {}, - 'notes.diffs.renote': {}, - 'notes.diffs.withFile': {}, - 'users.total': { accumulate: true }, - 'users.inc': { range: 'small' }, - 'users.dec': { range: 'small' }, - 'following.total': { accumulate: true }, - 'following.inc': { range: 'small' }, - 'following.dec': { range: 'small' }, - 'followers.total': { accumulate: true }, - 'followers.inc': { range: 'small' }, - 'followers.dec': { range: 'small' }, - 'drive.totalFiles': { accumulate: true }, - 'drive.incFiles': {}, - 'drive.decFiles': {}, - 'drive.incUsage': {}, // in kilobyte - 'drive.decUsage': {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/notes.ts b/packages/backend/src/services/chart/charts/entities/notes.ts deleted file mode 100644 index 9387dbfb2c..0000000000 --- a/packages/backend/src/services/chart/charts/entities/notes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'notes'; - -export const schema = { - 'local.total': { accumulate: true }, - 'local.inc': {}, - 'local.dec': {}, - 'local.diffs.normal': {}, - 'local.diffs.reply': {}, - 'local.diffs.renote': {}, - 'local.diffs.withFile': {}, - 'remote.total': { accumulate: true }, - 'remote.inc': {}, - 'remote.dec': {}, - 'remote.diffs.normal': {}, - 'remote.diffs.reply': {}, - 'remote.diffs.renote': {}, - 'remote.diffs.withFile': {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-drive.ts b/packages/backend/src/services/chart/charts/entities/per-user-drive.ts deleted file mode 100644 index 6111640ea0..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-drive.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'perUserDrive'; - -export const schema = { - 'totalCount': { accumulate: true }, - 'totalSize': { accumulate: true }, // in kilobyte - 'incCount': { range: 'small' }, - 'incSize': {}, // in kilobyte - 'decCount': { range: 'small' }, - 'decSize': {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-following.ts b/packages/backend/src/services/chart/charts/entities/per-user-following.ts deleted file mode 100644 index 4118daa474..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-following.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'perUserFollowing'; - -export const schema = { - 'local.followings.total': { accumulate: true }, - 'local.followings.inc': { range: 'small' }, - 'local.followings.dec': { range: 'small' }, - 'local.followers.total': { accumulate: true }, - 'local.followers.inc': { range: 'small' }, - 'local.followers.dec': { range: 'small' }, - 'remote.followings.total': { accumulate: true }, - 'remote.followings.inc': { range: 'small' }, - 'remote.followings.dec': { range: 'small' }, - 'remote.followers.total': { accumulate: true }, - 'remote.followers.inc': { range: 'small' }, - 'remote.followers.dec': { range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-notes.ts b/packages/backend/src/services/chart/charts/entities/per-user-notes.ts deleted file mode 100644 index c1fa174452..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-notes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'perUserNotes'; - -export const schema = { - 'total': { accumulate: true }, - 'inc': { range: 'small' }, - 'dec': { range: 'small' }, - 'diffs.normal': { range: 'small' }, - 'diffs.reply': { range: 'small' }, - 'diffs.renote': { range: 'small' }, - 'diffs.withFile': { range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts deleted file mode 100644 index 5e1a6c7b30..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'perUserReaction'; - -export const schema = { - 'local.count': { range: 'small' }, - 'remote.count': { range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/test-grouped.ts b/packages/backend/src/services/chart/charts/entities/test-grouped.ts deleted file mode 100644 index 66b6e8e864..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-grouped.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'testGrouped'; - -export const schema = { - 'foo.total': { accumulate: true }, - 'foo.inc': {}, - 'foo.dec': {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/test-intersection.ts b/packages/backend/src/services/chart/charts/entities/test-intersection.ts deleted file mode 100644 index a3bdcb367f..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-intersection.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'testIntersection'; - -export const schema = { - 'a': { uniqueIncrement: true }, - 'b': { uniqueIncrement: true }, - 'aAndB': { intersection: ['a', 'b'] }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/test-unique.ts b/packages/backend/src/services/chart/charts/entities/test-unique.ts deleted file mode 100644 index b2cfb71b05..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-unique.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'testUnique'; - -export const schema = { - 'foo': { uniqueIncrement: true }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/test.ts b/packages/backend/src/services/chart/charts/entities/test.ts deleted file mode 100644 index 7cba21e16a..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'test'; - -export const schema = { - 'foo.total': { accumulate: true }, - 'foo.inc': {}, - 'foo.dec': {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/users.ts b/packages/backend/src/services/chart/charts/entities/users.ts deleted file mode 100644 index c0b83094ae..0000000000 --- a/packages/backend/src/services/chart/charts/entities/users.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'users'; - -export const schema = { - 'local.total': { accumulate: true }, - 'local.inc': { range: 'small' }, - 'local.dec': { range: 'small' }, - 'remote.total': { accumulate: true }, - 'remote.inc': { range: 'small' }, - 'remote.dec': { range: 'small' }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/federation.ts b/packages/backend/src/services/chart/charts/federation.ts deleted file mode 100644 index 10221ee1e3..0000000000 --- a/packages/backend/src/services/chart/charts/federation.ts +++ /dev/null @@ -1,103 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { Followings, Instances } from '@/models/index.js'; -import { name, schema } from './entities/federation.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; - -/** - * フェデレーションに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class FederationChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return { - }; - } - - protected async tickMinor(): Promise>> { - const meta = await fetchMeta(); - - const suspendedInstancesQuery = Instances.createQueryBuilder('instance') - .select('instance.host') - .where('instance.isSuspended = true'); - - const pubsubSubQuery = Followings.createQueryBuilder('f') - .select('f.followerHost') - .where('f.followerHost IS NOT NULL'); - - const subInstancesQuery = Followings.createQueryBuilder('f') - .select('f.followeeHost') - .where('f.followeeHost IS NOT NULL'); - - const pubInstancesQuery = Followings.createQueryBuilder('f') - .select('f.followerHost') - .where('f.followerHost IS NOT NULL'); - - const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([ - Followings.createQueryBuilder('following') - .select('COUNT(DISTINCT following.followeeHost)') - .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) - .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) - .getRawOne() - .then(x => parseInt(x.count, 10)), - Followings.createQueryBuilder('following') - .select('COUNT(DISTINCT following.followerHost)') - .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followerHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) - .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) - .getRawOne() - .then(x => parseInt(x.count, 10)), - Followings.createQueryBuilder('following') - .select('COUNT(DISTINCT following.followeeHost)') - .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) - .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) - .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) - .setParameters(pubsubSubQuery.getParameters()) - .getRawOne() - .then(x => parseInt(x.count, 10)), - Instances.createQueryBuilder('instance') - .select('COUNT(instance.id)') - .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `instance.host NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) - .andWhere(`instance.isSuspended = false`) - .andWhere(`instance.lastCommunicatedAt > :gt`, { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) - .getRawOne() - .then(x => parseInt(x.count, 10)), - Instances.createQueryBuilder('instance') - .select('COUNT(instance.id)') - .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `instance.host NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) - .andWhere(`instance.isSuspended = false`) - .andWhere(`instance.lastCommunicatedAt > :gt`, { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) - .getRawOne() - .then(x => parseInt(x.count, 10)), - ]); - - return { - 'sub': sub, - 'pub': pub, - 'pubsub': pubsub, - 'subActive': subActive, - 'pubActive': pubActive, - }; - } - - public async deliverd(host: string, succeeded: boolean): Promise { - await this.commit(succeeded ? { - 'deliveredInstances': [host], - } : { - 'stalled': [host], - }); - } - - public async inbox(host: string): Promise { - await this.commit({ - 'inboxInstances': [host], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/hashtag.ts b/packages/backend/src/services/chart/charts/hashtag.ts deleted file mode 100644 index 31f7fa95dc..0000000000 --- a/packages/backend/src/services/chart/charts/hashtag.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { name, schema } from './entities/hashtag.js'; - -/** - * ハッシュタグに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class HashtagChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise { - await this.commit({ - 'local.users': Users.isLocalUser(user) ? [user.id] : [], - 'remote.users': Users.isLocalUser(user) ? [] : [user.id], - }, hashtag); - } -} diff --git a/packages/backend/src/services/chart/charts/instance.ts b/packages/backend/src/services/chart/charts/instance.ts deleted file mode 100644 index fe29ba5228..0000000000 --- a/packages/backend/src/services/chart/charts/instance.ts +++ /dev/null @@ -1,103 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { DriveFiles, Followings, Users, Notes } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { name, schema } from './entities/instance.js'; - -/** - * インスタンスごとのチャート - */ -// eslint-disable-next-line import/no-default-export -export default class InstanceChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - const [ - notesCount, - usersCount, - followingCount, - followersCount, - driveFiles, - ] = await Promise.all([ - Notes.countBy({ userHost: group }), - Users.countBy({ host: group }), - Followings.countBy({ followerHost: group }), - Followings.countBy({ followeeHost: group }), - DriveFiles.countBy({ userHost: group }), - ]); - - return { - 'notes.total': notesCount, - 'users.total': usersCount, - 'following.total': followingCount, - 'followers.total': followersCount, - 'drive.totalFiles': driveFiles, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async requestReceived(host: string): Promise { - await this.commit({ - 'requests.received': 1, - }, toPuny(host)); - } - - public async requestSent(host: string, isSucceeded: boolean): Promise { - await this.commit({ - 'requests.succeeded': isSucceeded ? 1 : 0, - 'requests.failed': isSucceeded ? 0 : 1, - }, toPuny(host)); - } - - public async newUser(host: string): Promise { - await this.commit({ - 'users.total': 1, - 'users.inc': 1, - }, toPuny(host)); - } - - public async updateNote(host: string, note: Note, isAdditional: boolean): Promise { - await this.commit({ - 'notes.total': isAdditional ? 1 : -1, - 'notes.inc': isAdditional ? 1 : 0, - 'notes.dec': isAdditional ? 0 : 1, - 'notes.diffs.normal': note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, - 'notes.diffs.renote': note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - 'notes.diffs.reply': note.replyId != null ? (isAdditional ? 1 : -1) : 0, - 'notes.diffs.withFile': note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }, toPuny(host)); - } - - public async updateFollowing(host: string, isAdditional: boolean): Promise { - await this.commit({ - 'following.total': isAdditional ? 1 : -1, - 'following.inc': isAdditional ? 1 : 0, - 'following.dec': isAdditional ? 0 : 1, - }, toPuny(host)); - } - - public async updateFollowers(host: string, isAdditional: boolean): Promise { - await this.commit({ - 'followers.total': isAdditional ? 1 : -1, - 'followers.inc': isAdditional ? 1 : 0, - 'followers.dec': isAdditional ? 0 : 1, - }, toPuny(host)); - } - - public async updateDrive(file: DriveFile, isAdditional: boolean): Promise { - const fileSizeKb = file.size / 1000; - await this.commit({ - 'drive.totalFiles': isAdditional ? 1 : -1, - 'drive.incFiles': isAdditional ? 1 : 0, - 'drive.incUsage': isAdditional ? fileSizeKb : 0, - 'drive.decFiles': isAdditional ? 1 : 0, - 'drive.decUsage': isAdditional ? fileSizeKb : 0, - }, file.userHost); - } -} diff --git a/packages/backend/src/services/chart/charts/notes.ts b/packages/backend/src/services/chart/charts/notes.ts deleted file mode 100644 index bb14b62f3c..0000000000 --- a/packages/backend/src/services/chart/charts/notes.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { Notes } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { Note } from '@/models/entities/note.js'; -import { name, schema } from './entities/notes.js'; - -/** - * ノートに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class NotesChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - const [localCount, remoteCount] = await Promise.all([ - Notes.countBy({ userHost: IsNull() }), - Notes.countBy({ userHost: Not(IsNull()) }), - ]); - - return { - 'local.total': localCount, - 'remote.total': remoteCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(note: Note, isAdditional: boolean): Promise { - const prefix = note.userHost === null ? 'local' : 'remote'; - - await this.commit({ - [`${prefix}.total`]: isAdditional ? 1 : -1, - [`${prefix}.inc`]: isAdditional ? 1 : 0, - [`${prefix}.dec`]: isAdditional ? 0 : 1, - [`${prefix}.diffs.normal`]: note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, - [`${prefix}.diffs.renote`]: note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - [`${prefix}.diffs.reply`]: note.replyId != null ? (isAdditional ? 1 : -1) : 0, - [`${prefix}.diffs.withFile`]: note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-drive.ts b/packages/backend/src/services/chart/charts/per-user-drive.ts deleted file mode 100644 index 5f75dc6887..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-drive.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { DriveFiles } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { name, schema } from './entities/per-user-drive.js'; - -/** - * ユーザーごとのドライブに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class PerUserDriveChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - const [count, size] = await Promise.all([ - DriveFiles.countBy({ userId: group }), - DriveFiles.calcDriveUsageOf(group), - ]); - - return { - 'totalCount': count, - 'totalSize': size, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(file: DriveFile, isAdditional: boolean): Promise { - const fileSizeKb = file.size / 1000; - await this.commit({ - 'totalCount': isAdditional ? 1 : -1, - 'totalSize': isAdditional ? fileSizeKb : -fileSizeKb, - 'incCount': isAdditional ? 1 : 0, - 'incSize': isAdditional ? fileSizeKb : 0, - 'decCount': isAdditional ? 0 : 1, - 'decSize': isAdditional ? 0 : fileSizeKb, - }, file.userId); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-following.ts b/packages/backend/src/services/chart/charts/per-user-following.ts deleted file mode 100644 index 02b149f52a..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-following.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { Followings, Users } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { name, schema } from './entities/per-user-following.js'; - -/** - * ユーザーごとのフォローに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class PerUserFollowingChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - const [ - localFollowingsCount, - localFollowersCount, - remoteFollowingsCount, - remoteFollowersCount, - ] = await Promise.all([ - Followings.countBy({ followerId: group, followeeHost: IsNull() }), - Followings.countBy({ followeeId: group, followerHost: IsNull() }), - Followings.countBy({ followerId: group, followeeHost: Not(IsNull()) }), - Followings.countBy({ followeeId: group, followerHost: Not(IsNull()) }), - ]); - - return { - 'local.followings.total': localFollowingsCount, - 'local.followers.total': localFollowersCount, - 'remote.followings.total': remoteFollowingsCount, - 'remote.followers.total': remoteFollowersCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise { - const prefixFollower = Users.isLocalUser(follower) ? 'local' : 'remote'; - const prefixFollowee = Users.isLocalUser(followee) ? 'local' : 'remote'; - - this.commit({ - [`${prefixFollower}.followings.total`]: isFollow ? 1 : -1, - [`${prefixFollower}.followings.inc`]: isFollow ? 1 : 0, - [`${prefixFollower}.followings.dec`]: isFollow ? 0 : 1, - }, follower.id); - this.commit({ - [`${prefixFollowee}.followers.total`]: isFollow ? 1 : -1, - [`${prefixFollowee}.followers.inc`]: isFollow ? 1 : 0, - [`${prefixFollowee}.followers.dec`]: isFollow ? 0 : 1, - }, followee.id); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-notes.ts b/packages/backend/src/services/chart/charts/per-user-notes.ts deleted file mode 100644 index b9191dd088..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-notes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { User } from '@/models/entities/user.js'; -import { Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { name, schema } from './entities/per-user-notes.js'; - -/** - * ユーザーごとのノートに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class PerUserNotesChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - const [count] = await Promise.all([ - Notes.countBy({ userId: group }), - ]); - - return { - total: count, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise { - await this.commit({ - 'total': isAdditional ? 1 : -1, - 'inc': isAdditional ? 1 : 0, - 'dec': isAdditional ? 0 : 1, - 'diffs.normal': note.replyId == null && note.renoteId == null ? (isAdditional ? 1 : -1) : 0, - 'diffs.renote': note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - 'diffs.reply': note.replyId != null ? (isAdditional ? 1 : -1) : 0, - 'diffs.withFile': note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }, user.id); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-reactions.ts b/packages/backend/src/services/chart/charts/per-user-reactions.ts deleted file mode 100644 index 3a830e118c..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-reactions.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Users } from '@/models/index.js'; -import { name, schema } from './entities/per-user-reactions.js'; - -/** - * ユーザーごとのリアクションに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class PerUserReactionsChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise { - const prefix = Users.isLocalUser(user) ? 'local' : 'remote'; - this.commit({ - [`${prefix}.count`]: 1, - }, note.userId); - } -} diff --git a/packages/backend/src/services/chart/charts/test-grouped.ts b/packages/backend/src/services/chart/charts/test-grouped.ts deleted file mode 100644 index d01c9fcbd6..0000000000 --- a/packages/backend/src/services/chart/charts/test-grouped.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-grouped.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -export default class TestGroupedChart extends Chart { - private total = {} as Record; - - constructor() { - super(name, schema, true); - } - - protected async tickMajor(group: string): Promise>> { - return { - 'foo.total': this.total[group], - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async increment(group: string): Promise { - if (this.total[group] == null) this.total[group] = 0; - - this.total[group]++; - - await this.commit({ - 'foo.total': 1, - 'foo.inc': 1, - }, group); - } -} diff --git a/packages/backend/src/services/chart/charts/test-intersection.ts b/packages/backend/src/services/chart/charts/test-intersection.ts deleted file mode 100644 index 88b5a715c1..0000000000 --- a/packages/backend/src/services/chart/charts/test-intersection.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-intersection.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -export default class TestIntersectionChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async addA(key: string): Promise { - await this.commit({ - a: [key], - }); - } - - public async addB(key: string): Promise { - await this.commit({ - b: [key], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/test-unique.ts b/packages/backend/src/services/chart/charts/test-unique.ts deleted file mode 100644 index d714f1d40c..0000000000 --- a/packages/backend/src/services/chart/charts/test-unique.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-unique.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -export default class TestUniqueChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async uniqueIncrement(key: string): Promise { - await this.commit({ - foo: [key], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/test.ts b/packages/backend/src/services/chart/charts/test.ts deleted file mode 100644 index adb2b18c87..0000000000 --- a/packages/backend/src/services/chart/charts/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -export default class TestChart extends Chart { - public total = 0; // publicにするのはテストのため - - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return { - 'foo.total': this.total, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async increment(): Promise { - this.total++; - - await this.commit({ - 'foo.total': 1, - 'foo.inc': 1, - }); - } - - public async decrement(): Promise { - this.total--; - - await this.commit({ - 'foo.total': -1, - 'foo.dec': 1, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/users.ts b/packages/backend/src/services/chart/charts/users.ts deleted file mode 100644 index acb16ead87..0000000000 --- a/packages/backend/src/services/chart/charts/users.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { Users } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { name, schema } from './entities/users.js'; - -/** - * ユーザー数に関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class UsersChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - const [localCount, remoteCount] = await Promise.all([ - Users.countBy({ host: IsNull() }), - Users.countBy({ host: Not(IsNull()) }), - ]); - - return { - 'local.total': localCount, - 'remote.total': remoteCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise { - const prefix = Users.isLocalUser(user) ? 'local' : 'remote'; - - await this.commit({ - [`${prefix}.total`]: isAdditional ? 1 : -1, - [`${prefix}.inc`]: isAdditional ? 1 : 0, - [`${prefix}.dec`]: isAdditional ? 0 : 1, - }); - } -} diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts deleted file mode 100644 index 2960bac8f7..0000000000 --- a/packages/backend/src/services/chart/core.ts +++ /dev/null @@ -1,677 +0,0 @@ -/** - * チャートエンジン - * - * Tests located in test/chart - */ - -import * as nestedProperty from 'nested-property'; -import Logger from '../logger.js'; -import { EntitySchema, Repository, LessThan, Between } from 'typeorm'; -import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js'; -import { getChartInsertLock } from '@/misc/app-lock.js'; -import { db } from '@/db/postgre.js'; - -const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); - -const columnPrefix = '___' as const; -const uniqueTempColumnPrefix = 'unique_temp___' as const; -const columnDot = '_' as const; - -type Schema = Record; - - range?: 'big' | 'small' | 'medium'; - - // previousな値を引き継ぐかどうか - accumulate?: boolean; -}>; - -type KeyToColumnName = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof columnDot}${KeyToColumnName}` : T; - -type Columns = { - [K in keyof S as `${typeof columnPrefix}${KeyToColumnName}`]: number; -}; - -type TempColumnsForUnique = { - [K in keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName}`]: S[K]['uniqueIncrement'] extends true ? string[] : never; -}; - -type RawRecord = { - id: number; - - /** - * 集計のグループ - */ - group?: string | null; - - /** - * 集計日時のUnixタイムスタンプ(秒) - */ - date: number; -} & TempColumnsForUnique & Columns; - -const camelToSnake = (str: string): string => { - return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); -}; - -const removeDuplicates = (array: any[]) => Array.from(new Set(array)); - -type Commit = { - [K in keyof S]?: S[K]['uniqueIncrement'] extends true ? string[] : number; -}; - -export type KVs = { - [K in keyof S]: number; -}; - -type ChartResult = { - [P in keyof T]: number[]; -}; - -type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; - -type UnflattenSingleton = K extends `${infer A}.${infer B}` - ? { [_ in A]: UnflattenSingleton; } - : { [_ in K]: V; }; - -type Unflatten> = UnionToIntersection< - { - [K in Extract]: UnflattenSingleton; - }[Extract] ->; - -type ToJsonSchema = { - type: 'object'; - properties: { - [K in keyof S]: S[K] extends number[] ? { type: 'array'; items: { type: 'number'; }; } : ToJsonSchema; - }, - required: (keyof S)[]; -}; - -export function getJsonSchema(schema: S): ToJsonSchema>> { - const jsonSchema = { - type: 'object', - properties: {} as Record, - required: [], - }; - - for (const k in schema) { - jsonSchema.properties[k] = { - type: 'array', - items: { type: 'number' }, - }; - } - - return jsonSchema as ToJsonSchema>>; -} - -/** - * 様々なチャートの管理を司るクラス - */ -// eslint-disable-next-line import/no-default-export -export default abstract class Chart { - public schema: T; - - private name: string; - private buffer: { - diff: Commit; - group: string | null; - }[] = []; - // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository>; - //private repositoryForDay: Repository>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; - - /** - * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) - */ - protected abstract tickMajor(group: string | null): Promise>>; - - /** - * 少なくとも最小スパン内に1回は実行されて欲しい計算処理を入れる - */ - protected abstract tickMinor(group: string | null): Promise>>; - - private static convertSchemaToColumnDefinitions(schema: Schema): Record { - const columns = {} as Record; - for (const [k, v] of Object.entries(schema)) { - const name = k.replaceAll('.', columnDot); - const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer'; - if (v.uniqueIncrement) { - columns[uniqueTempColumnPrefix + name] = { - type: 'varchar', - array: true, - default: '{}', - }; - columns[columnPrefix + name] = { - type, - default: 0, - }; - } else { - columns[columnPrefix + name] = { - type, - default: 0, - }; - } - } - return columns; - } - - private static dateToTimestamp(x: Date): number { - return Math.floor(x.getTime() / 1000); - } - - private static parseDate(date: Date): [number, number, number, number, number, number, number] { - const y = date.getUTCFullYear(); - const m = date.getUTCMonth(); - const d = date.getUTCDate(); - const h = date.getUTCHours(); - const _m = date.getUTCMinutes(); - const _s = date.getUTCSeconds(); - const _ms = date.getUTCMilliseconds(); - - return [y, m, d, h, _m, _s, _ms]; - } - - private static getCurrentDate() { - return Chart.parseDate(new Date()); - } - - public static schemaToEntity(name: string, schema: Schema, grouped = false): { - hour: EntitySchema, - day: EntitySchema, - } { - const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({ - name: - span === 'hour' ? `__chart__${camelToSnake(name)}` : - span === 'day' ? `__chart_day__${camelToSnake(name)}` : - new Error('not happen') as never, - columns: { - id: { - type: 'integer', - primary: true, - generated: true, - }, - date: { - type: 'integer', - }, - ...(grouped ? { - group: { - type: 'varchar', - length: 128, - }, - } : {}), - ...Chart.convertSchemaToColumnDefinitions(schema), - }, - indices: [{ - columns: grouped ? ['date', 'group'] : ['date'], - unique: true, - }], - uniques: [{ - columns: grouped ? ['date', 'group'] : ['date'], - }], - relations: { - /* TODO - group: { - target: () => Foo, - type: 'many-to-one', - onDelete: 'CASCADE', - }, - */ - }, - }); - - return { - hour: createEntity('hour'), - day: createEntity('day'), - }; - } - - constructor(name: string, schema: T, grouped = false) { - this.name = name; - this.schema = schema; - - const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); - this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); - } - - private convertRawRecord(x: RawRecord): KVs { - const kvs = {} as Record; - for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns)[]) { - kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k]; - } - return kvs as KVs; - } - - 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]])[]) { - if (v.accumulate && latest) { - log[k] = latest[k]; - } else { - log[k] = 0; - } - } - return log as KVs; - } - - private getLatestLog(group: string | null, span: 'hour' | 'day'): Promise | null> { - const repository = - span === 'hour' ? this.repositoryForHour : - span === 'day' ? this.repositoryForDay : - new Error('not happen') as never; - - return repository.findOne({ - where: group ? { - group: group, - } : {}, - order: { - date: -1, - }, - }).then(x => x ?? null) as Promise | null>; - } - - /** - * 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。 - */ - private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { - const [y, m, d, h] = Chart.getCurrentDate(); - - const current = dateUTC( - span === 'hour' ? [y, m, d, h] : - span === 'day' ? [y, m, d] : - new Error('not happen') as never); - - const repository = - span === 'hour' ? this.repositoryForHour : - span === 'day' ? this.repositoryForDay : - new Error('not happen') as never; - - // 現在(=今のHour or Day)のログ - const currentLog = await repository.findOneBy({ - date: Chart.dateToTimestamp(current), - ...(group ? { group: group } : {}), - }) as RawRecord | undefined; - - // ログがあればそれを返して終了 - if (currentLog != null) { - return currentLog; - } - - let log: RawRecord; - let data: KVs; - - // 集計期間が変わってから、初めてのチャート更新なら - // 最も最近のログを持ってくる - // * 例えば集計期間が「日」である場合で考えると、 - // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 - // * 「昨日の」と決め打ちせずに「もっとも最近の」とします - const latest = await this.getLatestLog(group, span); - - if (latest != null) { - // 空ログデータを作成 - data = this.getNewLog(this.convertRawRecord(latest)); - } else { - // ログが存在しなかったら - // (Misskeyインスタンスを建てて初めてのチャート更新時など) - - // 初期ログデータを作成 - data = this.getNewLog(null); - - logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); - } - - const date = Chart.dateToTimestamp(current); - const lockKey = group ? `${this.name}:${date}:${span}:${group}` : `${this.name}:${date}:${span}`; - - const unlock = await getChartInsertLock(lockKey); - try { - // ロック内でもう1回チェックする - const currentLog = await repository.findOneBy({ - date: date, - ...(group ? { group: group } : {}), - }) as RawRecord | undefined; - - // ログがあればそれを返して終了 - if (currentLog != null) return currentLog; - - const columns = {} as Record; - for (const [k, v] of Object.entries(data)) { - const name = k.replaceAll('.', columnDot); - columns[columnPrefix + name] = v; - } - - // 新規ログ挿入 - log = await repository.insert({ - date: date, - ...(group ? { group: group } : {}), - ...columns, - }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord; - - logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); - - return log; - } finally { - unlock(); - } - } - - protected commit(diff: Commit, group: string | null = null): void { - for (const [k, v] of Object.entries(diff)) { - if (v == null || v === 0 || (Array.isArray(v) && v.length === 0)) delete diff[k]; - } - this.buffer.push({ - diff, group, - }); - } - - public async save(): Promise { - if (this.buffer.length === 0) { - logger.info(`${this.name}: Write skipped`); - return; - } - - // TODO: 前の時間のログがbufferにあった場合のハンドリング - // 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。 - // 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、 - // そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。 - // これを回避するための実装は複雑になりそうなため、一旦保留。 - - const update = async (logHour: RawRecord, logDay: RawRecord): Promise => { - const finalDiffs = {} as Record; - - for (const diff of this.buffer.filter(q => q.group == null || (q.group === logHour.group)).map(q => q.diff)) { - for (const [k, v] of Object.entries(diff)) { - if (finalDiffs[k] == null) { - finalDiffs[k] = v; - } else { - if (typeof finalDiffs[k] === 'number') { - (finalDiffs[k] as number) += v as number; - } else { - (finalDiffs[k] as string[]) = (finalDiffs[k] as string[]).concat(v); - } - } - } - } - - const queryForHour: Record, number | (() => string)> = {} as any; - const queryForDay: Record, number | (() => string)> = {} as any; - for (const [k, v] of Object.entries(finalDiffs)) { - if (typeof v === 'number') { - const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns; - if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`; - if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`; - if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`; - if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`; - } else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント - const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - // TODO: item をSQLエスケープ - const itemsForHour = v.filter(item => !logHour[tempColumnName].includes(item)).map(item => `"${item}"`); - const itemsForDay = v.filter(item => !logDay[tempColumnName].includes(item)).map(item => `"${item}"`); - if (itemsForHour.length > 0) queryForHour[tempColumnName] = () => `array_cat("${tempColumnName}", '{${itemsForHour.join(',')}}'::varchar[])`; - if (itemsForDay.length > 0) queryForDay[tempColumnName] = () => `array_cat("${tempColumnName}", '{${itemsForDay.join(',')}}'::varchar[])`; - } - } - - // bake unique count - for (const [k, v] of Object.entries(finalDiffs)) { - if (this.schema[k].uniqueIncrement) { - const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns; - const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - queryForHour[name] = new Set([...(v as string[]), ...logHour[tempColumnName]]).size; - queryForDay[name] = new Set([...(v as string[]), ...logDay[tempColumnName]]).size; - } - } - - // compute intersection - // TODO: intersectionに指定されたカラムがintersectionだった場合の対応 - for (const [k, v] of Object.entries(this.schema)) { - const intersection = v.intersection; - if (intersection) { - const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns; - const firstKey = intersection[0]; - const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - const firstValues = finalDiffs[firstKey] as string[] | undefined; - const currentValuesForHour = new Set([...(firstValues ?? []), ...logHour[firstTempColumnName]]); - const currentValuesForDay = new Set([...(firstValues ?? []), ...logDay[firstTempColumnName]]); - for (let i = 1; i < intersection.length; i++) { - const targetKey = intersection[i]; - const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - const targetValues = finalDiffs[targetKey] as string[] | undefined; - const targetValuesForHour = new Set([...(targetValues ?? []), ...logHour[targetTempColumnName]]); - const targetValuesForDay = new Set([...(targetValues ?? []), ...logDay[targetTempColumnName]]); - currentValuesForHour.forEach(v => { - if (!targetValuesForHour.has(v)) currentValuesForHour.delete(v); - }); - currentValuesForDay.forEach(v => { - if (!targetValuesForDay.has(v)) currentValuesForDay.delete(v); - }); - } - queryForHour[name] = currentValuesForHour.size; - queryForDay[name] = currentValuesForDay.size; - } - } - - // ログ更新 - await Promise.all([ - this.repositoryForHour.createQueryBuilder() - .update() - .set(queryForHour as any) - .where('id = :id', { id: logHour.id }) - .execute(), - this.repositoryForDay.createQueryBuilder() - .update() - .set(queryForDay as any) - .where('id = :id', { id: logDay.id }) - .execute(), - ]); - - logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); - - // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする - this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); - }; - - const groups = removeDuplicates(this.buffer.map(log => log.group)); - - await Promise.all( - groups.map(group => - Promise.all([ - this.claimCurrentLog(group, 'hour'), - this.claimCurrentLog(group, 'day'), - ]).then(([logHour, logDay]) => - update(logHour, logDay)))); - } - - public async tick(major: boolean, group: string | null = null): Promise { - const data = major ? await this.tickMajor(group) : await this.tickMinor(group); - - const columns = {} as Record, number>; - for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) { - const name = columnPrefix + (k as string).replaceAll('.', columnDot) as keyof Columns; - columns[name] = v; - } - - if (Object.keys(columns).length === 0) { - return; - } - - const update = async (logHour: RawRecord, logDay: RawRecord): Promise => { - await Promise.all([ - this.repositoryForHour.createQueryBuilder() - .update() - .set(columns) - .where('id = :id', { id: logHour.id }) - .execute(), - this.repositoryForDay.createQueryBuilder() - .update() - .set(columns) - .where('id = :id', { id: logDay.id }) - .execute(), - ]); - }; - - return Promise.all([ - this.claimCurrentLog(group, 'hour'), - this.claimCurrentLog(group, 'day'), - ]).then(([logHour, logDay]) => - update(logHour, logDay)); - } - - public resync(group: string | null = null): Promise { - return this.tick(true, group); - } - - public async clean(): Promise { - const current = dateUTC(Chart.getCurrentDate()); - - // 一日以上前かつ三日以内 - const gt = Chart.dateToTimestamp(current) - (60 * 60 * 24 * 3); - const lt = Chart.dateToTimestamp(current) - (60 * 60 * 24); - - const columns = {} as Record, []>; - for (const [k, v] of Object.entries(this.schema)) { - if (v.uniqueIncrement) { - const name = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - columns[name] = []; - } - } - - if (Object.keys(columns).length === 0) { - return; - } - - await Promise.all([ - this.repositoryForHour.createQueryBuilder() - .update() - .set(columns) - .where('date > :gt', { gt }) - .andWhere('date < :lt', { lt }) - .execute(), - this.repositoryForDay.createQueryBuilder() - .update() - .set(columns) - .where('date > :gt', { gt }) - .andWhere('date < :lt', { lt }) - .execute(), - ]); - } - - 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; - - const lt = dateUTC([y, m, d, h, _m, _s, _ms]); - - const gt = - span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : - span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : - new Error('not happen') as never; - - const repository = - span === 'hour' ? this.repositoryForHour : - span === 'day' ? this.repositoryForDay : - new Error('not happen') as never; - - // ログ取得 - let logs = await repository.find({ - where: { - date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)), - ...(group ? { group: group } : {}), - }, - order: { - date: -1, - }, - }) as RawRecord[]; - - // 要求された範囲にログがひとつもなかったら - if (logs.length === 0) { - // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと隙間埋めできないため) - const recentLog = await repository.findOne({ - where: group ? { - group: group, - } : {}, - order: { - date: -1, - }, - }) as RawRecord | undefined; - - if (recentLog) { - logs = [recentLog]; - } - - // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { - // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (隙間埋めできないため) - const outdatedLog = await repository.findOne({ - where: { - date: LessThan(Chart.dateToTimestamp(gt)), - ...(group ? { group: group } : {}), - }, - order: { - date: -1, - }, - }) as RawRecord | undefined; - - if (outdatedLog) { - logs.push(outdatedLog); - } - } - - const chart: KVs[] = []; - - for (let i = (amount - 1); i >= 0; i--) { - const current = - span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') : - span === 'day' ? subtractTime(dateUTC([y, m, d]), i, 'day') : - new Error('not happen') as never; - - const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); - - if (log) { - chart.unshift(this.convertRawRecord(log)); - } else { - // 隙間埋め - const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); - const data = latest ? this.convertRawRecord(latest) : null; - chart.unshift(this.getNewLog(data)); - } - } - - const res = {} as ChartResult; - - /** - * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * を - * { foo: [1, 2, 3], bar: [5, 6, 7] } - * にする - */ - for (const record of chart) { - for (const [k, v] of Object.entries(record) as ([keyof typeof record, number])[]) { - if (res[k]) { - res[k].push(v); - } else { - res[k] = [v]; - } - } - } - - return res; - } - - 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 = {}; - for (const [k, v] of Object.entries(result)) { - nestedProperty.set(object, k, v); - } - return object as Unflatten>; - } -} diff --git a/packages/backend/src/services/chart/entities.ts b/packages/backend/src/services/chart/entities.ts deleted file mode 100644 index a9eeabd639..0000000000 --- a/packages/backend/src/services/chart/entities.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { entity as FederationChart } from './charts/entities/federation.js'; -import { entity as NotesChart } from './charts/entities/notes.js'; -import { entity as UsersChart } from './charts/entities/users.js'; -import { entity as ActiveUsersChart } from './charts/entities/active-users.js'; -import { entity as InstanceChart } from './charts/entities/instance.js'; -import { entity as PerUserNotesChart } from './charts/entities/per-user-notes.js'; -import { entity as DriveChart } from './charts/entities/drive.js'; -import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js'; -import { entity as HashtagChart } from './charts/entities/hashtag.js'; -import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js'; -import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; -import { entity as ApRequestChart } from './charts/entities/ap-request.js'; - -import { entity as TestChart } from './charts/entities/test.js'; -import { entity as TestGroupedChart } from './charts/entities/test-grouped.js'; -import { entity as TestUniqueChart } from './charts/entities/test-unique.js'; -import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js'; - -export const entities = [ - FederationChart.hour, FederationChart.day, - NotesChart.hour, NotesChart.day, - UsersChart.hour, UsersChart.day, - ActiveUsersChart.hour, ActiveUsersChart.day, - InstanceChart.hour, InstanceChart.day, - PerUserNotesChart.hour, PerUserNotesChart.day, - DriveChart.hour, DriveChart.day, - PerUserReactionsChart.hour, PerUserReactionsChart.day, - HashtagChart.hour, HashtagChart.day, - PerUserFollowingChart.hour, PerUserFollowingChart.day, - PerUserDriveChart.hour, PerUserDriveChart.day, - ApRequestChart.hour, ApRequestChart.day, - - ...(process.env.NODE_ENV === 'test' ? [ - TestChart.hour, TestChart.day, - TestGroupedChart.hour, TestGroupedChart.day, - TestUniqueChart.hour, TestUniqueChart.day, - TestIntersectionChart.hour, TestIntersectionChart.day, - ] : []), -]; diff --git a/packages/backend/src/services/chart/index.ts b/packages/backend/src/services/chart/index.ts deleted file mode 100644 index 8bf2d8f65f..0000000000 --- a/packages/backend/src/services/chart/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { beforeShutdown } from '@/misc/before-shutdown.js'; - -import FederationChart from './charts/federation.js'; -import NotesChart from './charts/notes.js'; -import UsersChart from './charts/users.js'; -import ActiveUsersChart from './charts/active-users.js'; -import InstanceChart from './charts/instance.js'; -import PerUserNotesChart from './charts/per-user-notes.js'; -import DriveChart from './charts/drive.js'; -import PerUserReactionsChart from './charts/per-user-reactions.js'; -import HashtagChart from './charts/hashtag.js'; -import PerUserFollowingChart from './charts/per-user-following.js'; -import PerUserDriveChart from './charts/per-user-drive.js'; -import ApRequestChart from './charts/ap-request.js'; - -export const federationChart = new FederationChart(); -export const notesChart = new NotesChart(); -export const usersChart = new UsersChart(); -export const activeUsersChart = new ActiveUsersChart(); -export const instanceChart = new InstanceChart(); -export const perUserNotesChart = new PerUserNotesChart(); -export const driveChart = new DriveChart(); -export const perUserReactionsChart = new PerUserReactionsChart(); -export const hashtagChart = new HashtagChart(); -export const perUserFollowingChart = new PerUserFollowingChart(); -export const perUserDriveChart = new PerUserDriveChart(); -export const apRequestChart = new ApRequestChart(); - -const charts = [ - federationChart, - notesChart, - usersChart, - activeUsersChart, - instanceChart, - perUserNotesChart, - driveChart, - perUserReactionsChart, - hashtagChart, - perUserFollowingChart, - perUserDriveChart, - apRequestChart, -]; - -// 20分おきにメモリ情報をDBに書き込み -setInterval(() => { - for (const chart of charts) { - chart.save(); - } -}, 1000 * 60 * 20); - -beforeShutdown(() => Promise.all(charts.map(chart => chart.save()))); diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts deleted file mode 100644 index d53a4235b8..0000000000 --- a/packages/backend/src/services/create-notification.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { User } from '@/models/entities/user.js'; -import { Notification } from '@/models/entities/notification.js'; -import { sendEmailNotification } from './send-email-notification.js'; - -export async function createNotification( - notifieeId: User['id'], - type: Notification['type'], - data: Partial -) { - if (data.notifierId && (notifieeId === data.notifierId)) { - return null; - } - - const profile = await UserProfiles.findOneBy({ userId: notifieeId }); - - const isMuted = profile?.mutingNotificationTypes.includes(type); - - // Create notification - const notification = await Notifications.insert({ - id: genId(), - createdAt: new Date(), - notifieeId: notifieeId, - type: type, - // 相手がこの通知をミュートしているようなら、既読を予めつけておく - isRead: isMuted, - ...data, - } as Partial) - .then(x => Notifications.findOneByOrFail(x.identifiers[0])); - - const packed = await Notifications.pack(notification, {}); - - // Publish notification event - publishMainStream(notifieeId, 'notification', packed); - - // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - setTimeout(async () => { - const fresh = await Notifications.findOneBy({ id: notification.id }); - if (fresh == null) return; // 既に削除されているかもしれない - if (fresh.isRead) return; - - //#region ただしミュートしているユーザーからの通知なら無視 - const mutings = await Mutings.findBy({ - muterId: notifieeId, - }); - if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { - return; - } - //#endregion - - publishMainStream(notifieeId, 'unreadNotification', packed); - pushNotification(notifieeId, 'notification', packed); - - if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); - if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); - }, 2000); - - return notification; -} diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts deleted file mode 100644 index bae91ec4c3..0000000000 --- a/packages/backend/src/services/create-system-user.ts +++ /dev/null @@ -1,68 +0,0 @@ -import bcrypt from 'bcryptjs'; -import { v4 as uuid } from 'uuid'; -import generateNativeUserToken from '../server/api/common/generate-native-user-token.js'; -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { IsNull } from 'typeorm'; -import { genId } from '@/misc/gen-id.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { UsedUsername } from '@/models/entities/used-username.js'; -import { db } from '@/db/postgre.js'; - -export async function createSystemUser(username: string) { - 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 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: 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; -} diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts deleted file mode 100644 index 0fdceb671b..0000000000 --- a/packages/backend/src/services/delete-account.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Users } from '@/models/index.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; -import { publishUserEvent } from './stream.js'; -import { doPostSuspend } from './suspend-user.js'; - -export async function deleteAccount(user: { - id: string; - host: string | null; -}): Promise { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - - await Users.update(user.id, { - isDeleted: true, - }); - - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); -} diff --git a/packages/backend/src/services/detect-sensitive.ts b/packages/backend/src/services/detect-sensitive.ts deleted file mode 100644 index 2ade39d524..0000000000 --- a/packages/backend/src/services/detect-sensitive.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as nsfw from 'nsfwjs'; -import si from 'systeminformation'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const REQUIRED_CPU_FLAGS = ['avx2', 'fma']; -let isSupportedCpu: undefined | boolean = undefined; - -let model: nsfw.NSFWJS; - -export async function detectSensitive(path: string): Promise { - try { - if (isSupportedCpu === undefined) { - const cpuFlags = await getCpuFlags(); - isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); - } - - if (!isSupportedCpu) { - console.error('This CPU cannot use TensorFlow.'); - return null; - } - - const tf = await import('@tensorflow/tfjs-node'); - - if (model == null) model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); - - const buffer = await fs.promises.readFile(path); - const image = await tf.node.decodeImage(buffer, 3) as any; - try { - const predictions = await model.classify(image); - return predictions; - } finally { - image.dispose(); - } - } catch (err) { - console.error(err); - return null; - } -} - -async function getCpuFlags(): Promise { - const str = await si.cpuFlags(); - return str.split(/\s+/); -} diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts deleted file mode 100644 index 709db88f2f..0000000000 --- a/packages/backend/src/services/drive/add-file.ts +++ /dev/null @@ -1,540 +0,0 @@ -import * as fs from 'node:fs'; - -import { v4 as uuid } from 'uuid'; - -import S3 from 'aws-sdk/clients/s3.js'; -import sharp from 'sharp'; -import { IsNull } from 'typeorm'; -import { publishMainStream, publishDriveStream } from '@/services/stream.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { contentDisposition } from '@/misc/content-disposition.js'; -import { getFileInfo } from '@/misc/get-file-info.js'; -import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { IRemoteUser, User } from '@/models/entities/user.js'; -import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getS3 } from './s3.js'; -import { InternalStorage } from './internal-storage.js'; -import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; -import { driveLogger } from './logger.js'; -import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; -import { deleteFile } from './delete-file.js'; - -const logger = driveLogger.createSubLogger('register', 'yellow'); - -/*** - * Save file - * @param path Path for original - * @param name Name for original - * @param type Content-Type for original - * @param hash Hash for original - * @param size Size for original - */ -async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { - // thunbnail, webpublic を必要なら生成 - const alts = await generateAlts(path, type, !file.uri); - - const meta = await fetchMeta(); - - if (meta.useObjectStorage) { - //#region ObjectStorage params - let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); - - if (ext === '') { - if (type === 'image/jpeg') ext = '.jpg'; - if (type === 'image/png') ext = '.png'; - if (type === 'image/webp') ext = '.webp'; - if (type === 'image/apng') ext = '.apng'; - if (type === 'image/vnd.mozilla.apng') ext = '.apng'; - } - - // 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、 - // 許可されているファイル形式でしか拡張子をつけない - if (!FILE_TYPE_BROWSERSAFE.includes(type)) { - ext = ''; - } - - const baseUrl = meta.objectStorageBaseUrl - || `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; - - // for original - const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; - const url = `${ baseUrl }/${ key }`; - - // for alts - let webpublicKey: string | null = null; - let webpublicUrl: string | null = null; - let thumbnailKey: string | null = null; - let thumbnailUrl: string | null = null; - //#endregion - - //#region Uploads - logger.info(`uploading original: ${key}`); - const uploads = [ - upload(key, fs.createReadStream(path), type, name), - ]; - - if (alts.webpublic) { - webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; - webpublicUrl = `${ baseUrl }/${ webpublicKey }`; - - logger.info(`uploading webpublic: ${webpublicKey}`); - uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name)); - } - - if (alts.thumbnail) { - thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; - thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; - - logger.info(`uploading thumbnail: ${thumbnailKey}`); - uploads.push(upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type)); - } - - await Promise.all(uploads); - //#endregion - - file.url = url; - file.thumbnailUrl = thumbnailUrl; - file.webpublicUrl = webpublicUrl; - file.accessKey = key; - file.thumbnailAccessKey = thumbnailKey; - file.webpublicAccessKey = webpublicKey; - file.webpublicType = alts.webpublic?.type ?? null; - file.name = name; - file.type = type; - file.md5 = hash; - file.size = size; - file.storedInternal = false; - - return await DriveFiles.insert(file).then(x => DriveFiles.findOneByOrFail(x.identifiers[0])); - } else { // use internal storage - const accessKey = uuid(); - const thumbnailAccessKey = 'thumbnail-' + uuid(); - const webpublicAccessKey = 'webpublic-' + uuid(); - - const url = InternalStorage.saveFromPath(accessKey, path); - - let thumbnailUrl: string | null = null; - let webpublicUrl: string | null = null; - - if (alts.thumbnail) { - thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); - logger.info(`thumbnail stored: ${thumbnailAccessKey}`); - } - - if (alts.webpublic) { - webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); - logger.info(`web stored: ${webpublicAccessKey}`); - } - - file.storedInternal = true; - file.url = url; - file.thumbnailUrl = thumbnailUrl; - file.webpublicUrl = webpublicUrl; - file.accessKey = accessKey; - file.thumbnailAccessKey = thumbnailAccessKey; - file.webpublicAccessKey = webpublicAccessKey; - file.webpublicType = alts.webpublic?.type ?? null; - file.name = name; - file.type = type; - file.md5 = hash; - file.size = size; - - return await DriveFiles.insert(file).then(x => DriveFiles.findOneByOrFail(x.identifiers[0])); - } -} - -/** - * Generate webpublic, thumbnail, etc - * @param path Path for original - * @param type Content-Type for original - * @param generateWeb Generate webpublic or not - */ -export async function generateAlts(path: string, type: string, generateWeb: boolean) { - if (type.startsWith('video/')) { - try { - const thumbnail = await GenerateVideoThumbnail(path); - return { - webpublic: null, - thumbnail, - }; - } catch (err) { - logger.warn(`GenerateVideoThumbnail failed: ${err}`); - return { - webpublic: null, - thumbnail: null, - }; - } - } - - if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) { - logger.debug('web image and thumbnail not created (not an required file)'); - return { - webpublic: null, - thumbnail: null, - }; - } - - let img: sharp.Sharp | null = null; - let satisfyWebpublic: boolean; - - try { - img = sharp(path); - const metadata = await img.metadata(); - const isAnimated = metadata.pages && metadata.pages > 1; - - // skip animated - if (isAnimated) { - return { - webpublic: null, - thumbnail: null, - }; - } - - satisfyWebpublic = !!( - type !== 'image/svg+xml' && type !== 'image/webp' && - !(metadata.exif || metadata.iptc || metadata.xmp || metadata.tifftagPhotoshop) && - metadata.width && metadata.width <= 2048 && - metadata.height && metadata.height <= 2048 - ); - } catch (err) { - logger.warn(`sharp failed: ${err}`); - return { - webpublic: null, - thumbnail: null, - }; - } - - // #region webpublic - let webpublic: IImage | null = null; - - if (generateWeb && !satisfyWebpublic) { - logger.info('creating web image'); - - try { - if (['image/jpeg', 'image/webp'].includes(type)) { - webpublic = await convertSharpToJpeg(img, 2048, 2048); - } else if (['image/png'].includes(type)) { - webpublic = await convertSharpToPng(img, 2048, 2048); - } else if (['image/svg+xml'].includes(type)) { - webpublic = await convertSharpToPng(img, 2048, 2048); - } else { - logger.debug('web image not created (not an required image)'); - } - } catch (err) { - logger.warn('web image not created (an error occured)', err as Error); - } - } else { - if (satisfyWebpublic) logger.info('web image not created (original satisfies webpublic)'); - else logger.info('web image not created (from remote)'); - } - // #endregion webpublic - - // #region thumbnail - let thumbnail: IImage | null = null; - - try { - if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { - thumbnail = await convertSharpToWebp(img, 498, 280); - } else { - logger.debug('thumbnail not created (not an required file)'); - } - } catch (err) { - logger.warn('thumbnail not created (an error occured)', err as Error); - } - // #endregion thumbnail - - return { - webpublic, - thumbnail, - }; -} - -/** - * Upload to ObjectStorage - */ -async function 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'; - - const meta = await fetchMeta(); - - const params = { - Bucket: meta.objectStorageBucket, - Key: key, - Body: stream, - ContentType: type, - CacheControl: 'max-age=31536000, immutable', - } as S3.PutObjectRequest; - - if (filename) params.ContentDisposition = contentDisposition('inline', filename); - if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - - const s3 = getS3(meta); - - const upload = s3.upload(params, { - partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, - }); - - const result = await upload.promise(); - if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); -} - -async function deleteOldFile(user: IRemoteUser) { - const q = DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .andWhere('file.isLink = FALSE'); - - if (user.avatarId) { - q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); - } - - if (user.bannerId) { - q.andWhere('file.id != :bannerId', { bannerId: user.bannerId }); - } - - q.orderBy('file.id', 'ASC'); - - const oldFile = await q.getOne(); - - if (oldFile) { - deleteFile(oldFile, true); - } -} - -type AddFileArgs = { - /** User who wish to add file */ - user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; - /** File path */ - path: string; - /** Name */ - name?: string | null; - /** Comment */ - comment?: string | null; - /** Folder ID */ - folderId?: any; - /** If set to true, forcibly upload the file even if there is a file with the same hash. */ - force?: boolean; - /** Do not save file to local */ - isLink?: boolean; - /** URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL) */ - url?: string | null; - /** URL of source (リモートインスタンスのURLからアップロードされた場合の元URL) */ - uri?: string | null; - /** Mark file as sensitive */ - sensitive?: boolean | null; - - requestIp?: string | null; - requestHeaders?: Record | null; -}; - -/** - * Add file to drive - * - */ -export async function addFile({ - user, - path, - name = null, - comment = null, - folderId = null, - force = false, - isLink = false, - url = null, - uri = null, - sensitive = null, - requestIp = null, - requestHeaders = null, -}: AddFileArgs): Promise { - let skipNsfwCheck = false; - const instance = await fetchMeta(); - if (user == null) skipNsfwCheck = true; - if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'local' && Users.isRemoteUser(user)) skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'remote' && Users.isLocalUser(user)) skipNsfwCheck = true; - - const info = await getFileInfo(path, { - skipSensitiveDetection: skipNsfwCheck, - sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : - 0.5, - sensitiveThresholdForPorn: 0.75, - enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, - }); - logger.info(`${JSON.stringify(info)}`); - - // 現状 false positive が多すぎて実用に耐えない - //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { - // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); - //} - - // detect name - const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); - - if (user && !force) { - // Check if there is a file with the same hash - const much = await DriveFiles.findOneBy({ - md5: info.md5, - userId: user.id, - }); - - if (much) { - logger.info(`file with same hash is found: ${much.id}`); - return much; - } - } - - //#region Check drive usage - if (user && !isLink) { - const usage = await DriveFiles.calcDriveUsageOf(user); - const u = await Users.findOneBy({ id: user.id }); - - const instance = await fetchMeta(); - let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); - - if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { - driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; - logger.debug('drive capacity override applied'); - logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); - } - - logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); - - // If usage limit exceeded - if (usage + info.size > driveCapacity) { - if (Users.isLocalUser(user)) { - throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); - } else { - // (アバターまたはバナーを含まず)最も古いファイルを削除する - deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); - } - } - } - //#endregion - - const fetchFolder = async () => { - if (!folderId) { - return null; - } - - const driveFolder = await DriveFolders.findOneBy({ - id: folderId, - userId: user ? user.id : IsNull(), - }); - - if (driveFolder == null) throw new Error('folder-not-found'); - - return driveFolder; - }; - - const properties: { - width?: number; - height?: number; - orientation?: number; - } = {}; - - if (info.width) { - properties['width'] = info.width; - properties['height'] = info.height; - } - if (info.orientation != null) { - properties['orientation'] = info.orientation; - } - - const profile = user ? await UserProfiles.findOneBy({ userId: user.id }) : null; - - const folder = await fetchFolder(); - - let file = new DriveFile(); - file.id = genId(); - file.createdAt = new Date(); - file.userId = user ? user.id : null; - file.userHost = user ? user.host : null; - file.folderId = folder !== null ? folder.id : null; - file.comment = comment; - file.properties = properties; - file.blurhash = info.blurhash || null; - file.isLink = isLink; - file.requestIp = requestIp; - file.requestHeaders = requestHeaders; - file.maybeSensitive = info.sensitive; - file.maybePorn = info.porn; - file.isSensitive = user - ? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false - : false; - - if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; - if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; - - if (url !== null) { - file.src = url; - - if (isLink) { - file.url = url; - // ローカルプロキシ用 - file.accessKey = uuid(); - file.thumbnailAccessKey = 'thumbnail-' + uuid(); - file.webpublicAccessKey = 'webpublic-' + uuid(); - } - } - - if (uri !== null) { - file.uri = uri; - } - - if (isLink) { - try { - file.size = 0; - file.md5 = info.md5; - file.name = detectedName; - file.type = info.type.mime; - file.storedInternal = false; - - file = await DriveFiles.insert(file).then(x => DriveFiles.findOneByOrFail(x.identifiers[0])); - } catch (err) { - // duplicate key error (when already registered) - if (isDuplicateKeyValueError(err)) { - logger.info(`already registered ${file.uri}`); - - file = await DriveFiles.findOneBy({ - uri: file.uri!, - userId: user ? user.id : IsNull(), - }) as DriveFile; - } else { - logger.error(err as Error); - throw err; - } - } - } else { - file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size)); - } - - logger.succ(`drive file has been created ${file.id}`); - - if (user) { - DriveFiles.pack(file, { self: true }).then(packedFile => { - // Publish driveFileCreated event - publishMainStream(user.id, 'driveFileCreated', packedFile); - publishDriveStream(user.id, 'fileCreated', packedFile); - }); - } - - // 統計を更新 - driveChart.update(file, true); - perUserDriveChart.update(file, true); - if (file.userHost !== null) { - instanceChart.updateDrive(file, true); - } - - return file; -} diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts deleted file mode 100644 index 4816a3a31b..0000000000 --- a/packages/backend/src/services/drive/delete-file.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { InternalStorage } from './internal-storage.js'; -import { DriveFiles, Instances } from '@/models/index.js'; -import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js'; -import { createDeleteObjectStorageFileJob } from '@/queue/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { getS3 } from './s3.js'; -import { v4 as uuid } from 'uuid'; - -export async function deleteFile(file: DriveFile, isExpired = false) { - if (file.storedInternal) { - InternalStorage.del(file.accessKey!); - - if (file.thumbnailUrl) { - InternalStorage.del(file.thumbnailAccessKey!); - } - - if (file.webpublicUrl) { - InternalStorage.del(file.webpublicAccessKey!); - } - } else if (!file.isLink) { - createDeleteObjectStorageFileJob(file.accessKey!); - - if (file.thumbnailUrl) { - createDeleteObjectStorageFileJob(file.thumbnailAccessKey!); - } - - if (file.webpublicUrl) { - createDeleteObjectStorageFileJob(file.webpublicAccessKey!); - } - } - - postProcess(file, isExpired); -} - -export async function deleteFileSync(file: DriveFile, isExpired = false) { - if (file.storedInternal) { - InternalStorage.del(file.accessKey!); - - if (file.thumbnailUrl) { - InternalStorage.del(file.thumbnailAccessKey!); - } - - if (file.webpublicUrl) { - InternalStorage.del(file.webpublicAccessKey!); - } - } else if (!file.isLink) { - const promises = []; - - promises.push(deleteObjectStorageFile(file.accessKey!)); - - if (file.thumbnailUrl) { - promises.push(deleteObjectStorageFile(file.thumbnailAccessKey!)); - } - - if (file.webpublicUrl) { - promises.push(deleteObjectStorageFile(file.webpublicAccessKey!)); - } - - await Promise.all(promises); - } - - postProcess(file, isExpired); -} - -async function postProcess(file: DriveFile, isExpired = false) { - // リモートファイル期限切れ削除後は直リンクにする - if (isExpired && file.userHost !== null && file.uri != null) { - DriveFiles.update(file.id, { - isLink: true, - url: file.uri, - thumbnailUrl: null, - webpublicUrl: null, - storedInternal: false, - // ローカルプロキシ用 - accessKey: uuid(), - thumbnailAccessKey: 'thumbnail-' + uuid(), - webpublicAccessKey: 'webpublic-' + uuid(), - }); - } else { - DriveFiles.delete(file.id); - } - - // 統計を更新 - driveChart.update(file, false); - perUserDriveChart.update(file, false); - if (file.userHost !== null) { - instanceChart.updateDrive(file, false); - } -} - -export async function deleteObjectStorageFile(key: string) { - const meta = await fetchMeta(); - - const s3 = getS3(meta); - - await s3.deleteObject({ - Bucket: meta.objectStorageBucket!, - Key: key, - }).promise(); -} diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts deleted file mode 100644 index 6e6666481d..0000000000 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as fs from 'node:fs'; -import { createTempDir } from '@/misc/create-temp.js'; -import { IImage, convertToJpeg } from './image-processor.js'; -import FFmpeg from 'fluent-ffmpeg'; - -export async function GenerateVideoThumbnail(source: string): Promise { - const [dir, cleanup] = await createTempDir(); - - try { - await new Promise((res, rej) => { - FFmpeg({ - source, - }) - .on('end', res) - .on('error', rej) - .screenshot({ - folder: dir, - filename: 'out.png', // must have .png extension - count: 1, - timestamps: ['5%'], - }); - }); - - // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - return await convertToJpeg(`${dir}/out.png`, 498, 280); - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/services/drive/image-processor.ts b/packages/backend/src/services/drive/image-processor.ts deleted file mode 100644 index 2c564ea595..0000000000 --- a/packages/backend/src/services/drive/image-processor.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sharp from 'sharp'; - -export type IImage = { - data: Buffer; - ext: string | null; - type: string; -}; - -/** - * Convert to JPEG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToJpeg(path: string, width: number, height: number): Promise { - return convertSharpToJpeg(await sharp(path), width, height); -} - -export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .jpeg({ - quality: 85, - progressive: true, - }) - .toBuffer(); - - return { - data, - ext: 'jpg', - type: 'image/jpeg', - }; -} - -/** - * Convert to WebP - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToWebp(path: string, width: number, height: number, quality: number = 85): Promise { - return convertSharpToWebp(await sharp(path), width, height, quality); -} - -export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality: number = 85): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .webp({ - quality, - }) - .toBuffer(); - - return { - data, - ext: 'webp', - type: 'image/webp', - }; -} - -/** - * Convert to PNG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToPng(path: string, width: number, height: number): Promise { - return convertSharpToPng(await sharp(path), width, height); -} - -export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .png() - .toBuffer(); - - return { - data, - ext: 'png', - type: 'image/png', - }; -} diff --git a/packages/backend/src/services/drive/internal-storage.ts b/packages/backend/src/services/drive/internal-storage.ts deleted file mode 100644 index 8f76c81ca3..0000000000 --- a/packages/backend/src/services/drive/internal-storage.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as fs from 'node:fs'; -import * as Path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import config from '@/config/index.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -export class InternalStorage { - private static readonly path = Path.resolve(_dirname, '../../../../../files'); - - public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key); - - public static read(key: string) { - return fs.createReadStream(InternalStorage.resolvePath(key)); - } - - public static saveFromPath(key: string, srcPath: string) { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.copyFileSync(srcPath, InternalStorage.resolvePath(key)); - return `${config.url}/files/${key}`; - } - - public static saveFromBuffer(key: string, data: Buffer) { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.writeFileSync(InternalStorage.resolvePath(key), data); - return `${config.url}/files/${key}`; - } - - public static del(key: string) { - fs.unlink(InternalStorage.resolvePath(key), () => {}); - } -} diff --git a/packages/backend/src/services/drive/logger.ts b/packages/backend/src/services/drive/logger.ts deleted file mode 100644 index 917a8317e2..0000000000 --- a/packages/backend/src/services/drive/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '../logger.js'; - -export const driveLogger = new Logger('drive', 'blue'); diff --git a/packages/backend/src/services/drive/s3.ts b/packages/backend/src/services/drive/s3.ts deleted file mode 100644 index 80e34be956..0000000000 --- a/packages/backend/src/services/drive/s3.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { URL } from 'node:url'; -import S3 from 'aws-sdk/clients/s3.js'; -import { Meta } from '@/models/entities/meta.js'; -import { getAgentByUrl } from '@/misc/fetch.js'; - -export function getS3(meta: Meta) { - const u = meta.objectStorageEndpoint != null - ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` - : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; - - return new S3({ - endpoint: meta.objectStorageEndpoint || undefined, - accessKeyId: meta.objectStorageAccessKey!, - secretAccessKey: meta.objectStorageSecretKey!, - region: meta.objectStorageRegion || undefined, - sslEnabled: meta.objectStorageUseSSL, - s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted - ? false - : meta.objectStorageS3ForcePathStyle, - httpOptions: { - agent: getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), - }, - }); -} diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts deleted file mode 100644 index 3c5e1aa5c1..0000000000 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { URL } from 'node:url'; -import { User } from '@/models/entities/user.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; -import { driveLogger } from './logger.js'; -import { addFile } from './add-file.js'; - -const logger = driveLogger.createSubLogger('downloader'); - -type Args = { - url: string; - user: { id: User['id']; host: User['host'] } | null; - folderId?: DriveFolder['id'] | null; - uri?: string | null; - sensitive?: boolean; - force?: boolean; - isLink?: boolean; - comment?: string | null; - requestIp?: string | null; - requestHeaders?: Record | null; -}; - -export async function uploadFromUrl({ - url, - user, - folderId = null, - uri = null, - sensitive = false, - force = false, - isLink = false, - comment = null, - requestIp = null, - requestHeaders = null, -}: Args): Promise { - let name = new URL(url).pathname.split('/').pop() || null; - if (name == null || !DriveFiles.validateFileName(name)) { - name = null; - } - - // If the comment is same as the name, skip comment - // (image.name is passed in when receiving attachment) - if (comment !== null && name === comment) { - comment = null; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - try { - // write content at URL to temp file - await downloadUrl(url, path); - - const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); - logger.succ(`Got: ${driveFile.id}`); - return driveFile!; - } catch (e) { - logger.error(`Failed to create drive file: ${e}`, { - url: url, - e: e, - }); - throw e; - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts deleted file mode 100644 index ee1245132a..0000000000 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { DOMWindow, JSDOM } from 'jsdom'; -import fetch from 'node-fetch'; -import tinycolor from 'tinycolor2'; -import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; -import { Instance } from '@/models/entities/instance.js'; -import { Instances } from '@/models/index.js'; -import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js'; -import Logger from './logger.js'; -import { URL } from 'node:url'; - -const logger = new Logger('metadata', 'cyan'); - -export async function fetchInstanceMetadata(instance: Instance, force = false): Promise { - const unlock = await getFetchInstanceMetadataLock(instance.host); - - if (!force) { - const _instance = await Instances.findOneBy({ host: instance.host }); - const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { - unlock(); - return; - } - } - - logger.info(`Fetching metadata of ${instance.host} ...`); - - try { - const [info, dom, manifest] = await Promise.all([ - fetchNodeinfo(instance).catch(() => null), - fetchDom(instance).catch(() => null), - fetchManifest(instance).catch(() => null), - ]); - - const [favicon, icon, themeColor, name, description] = await Promise.all([ - fetchFaviconUrl(instance, dom).catch(() => null), - fetchIconUrl(instance, dom, manifest).catch(() => null), - getThemeColor(info, dom, manifest).catch(() => null), - getSiteName(info, dom, manifest).catch(() => null), - getDescription(info, dom, manifest).catch(() => null), - ]); - - logger.succ(`Successfuly fetched metadata of ${instance.host}`); - - const updates = { - infoUpdatedAt: new Date(), - } as Record; - - if (info) { - updates.softwareName = info.software?.name.toLowerCase(); - updates.softwareVersion = info.software?.version; - updates.openRegistrations = info.openRegistrations; - updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null; - updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null; - } - - if (name) updates.name = name; - if (description) updates.description = description; - if (icon || favicon) updates.iconUrl = icon || favicon; - if (favicon) updates.faviconUrl = favicon; - if (themeColor) updates.themeColor = themeColor; - - await Instances.update(instance.id, updates); - - logger.succ(`Successfuly updated metadata of ${instance.host}`); - } catch (e) { - logger.error(`Failed to update metadata of ${instance.host}: ${e}`); - } finally { - unlock(); - } -} - -type NodeInfo = { - openRegistrations?: any; - software?: { - name?: any; - version?: any; - }; - metadata?: { - name?: any; - nodeName?: any; - nodeDescription?: any; - description?: any; - maintainer?: { - name?: any; - email?: any; - }; - }; -}; - -async function fetchNodeinfo(instance: Instance): Promise { - logger.info(`Fetching nodeinfo of ${instance.host} ...`); - - try { - const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo') - .catch(e => { - if (e.statusCode === 404) { - throw 'No nodeinfo provided'; - } else { - throw e.statusCode || e.message; - } - }) as Record; - - if (wellknown.links == null || !Array.isArray(wellknown.links)) { - throw 'No wellknown links'; - } - - const links = wellknown.links as any[]; - - const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); - const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); - const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); - const link = lnik2_1 || lnik2_0 || lnik1_0; - - if (link == null) { - throw 'No nodeinfo link provided'; - } - - const info = await getJson(link.href) - .catch(e => { - throw e.statusCode || e.message; - }); - - logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); - - return info as NodeInfo; - } catch (e) { - logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`); - - throw e; - } -} - -async function fetchDom(instance: Instance): Promise { - logger.info(`Fetching HTML of ${instance.host} ...`); - - const url = 'https://' + instance.host; - - const html = await getHtml(url); - - const { window } = new JSDOM(html); - const doc = window.document; - - return doc; -} - -async function fetchManifest(instance: Instance): Promise | null> { - const url = 'https://' + instance.host; - - const manifestUrl = url + '/manifest.json'; - - const manifest = await getJson(manifestUrl) as Record; - - return manifest; -} - -async function fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise { - const url = 'https://' + instance.host; - - if (doc) { - // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 - const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; - - if (href) { - return (new URL(href, url)).href; - } - } - - const faviconUrl = url + '/favicon.ico'; - - const favicon = await fetch(faviconUrl, { - // TODO - //timeout: 10000, - agent: getAgentByUrl, - }); - - if (favicon.ok) { - return faviconUrl; - } - - return null; -} - -async function 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; - return (new URL(manifest.icons[0].src, url)).href; - } - - if (doc) { - const url = 'https://' + instance.host; - - // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 - const links = Array.from(doc.getElementsByTagName('link')).reverse(); - // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 - const href = - [ - links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, - links.find(link => link.relList.contains('apple-touch-icon'))?.href, - links.find(link => link.relList.contains('icon'))?.href, - ] - .find(href => href); - - if (href) { - return (new URL(href, url)).href; - } - } - - return null; -} - -async function 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; - - if (themeColor) { - const color = new tinycolor(themeColor); - if (color.isValid()) return color.toHexString(); - } - - return null; -} - -async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { - if (info && info.metadata) { - if (info.metadata.nodeName || info.metadata.name) { - return info.metadata.nodeName || info.metadata.name; - } - } - - if (doc) { - const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); - - if (og) { - return og; - } - } - - if (manifest) { - return manifest?.name || manifest?.short_name; - } - - return null; -} - -async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { - if (info && info.metadata) { - if (info.metadata.nodeDescription || info.metadata.description) { - return info.metadata.nodeDescription || info.metadata.description; - } - } - - if (doc) { - const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); - if (meta) { - return meta; - } - - const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); - if (og) { - return og; - } - } - - if (manifest) { - return manifest?.name || manifest?.short_name; - } - - return null; -} diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts deleted file mode 100644 index 72c24676bb..0000000000 --- a/packages/backend/src/services/following/create.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderAccept from '@/remote/activitypub/renderer/accept.js'; -import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { deliver } from '@/queue/index.js'; -import createFollowRequest from './requests/create.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import Logger from '../logger.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User } from '@/models/entities/user.js'; -import { Followings, Users, FollowRequests, Blockings, Instances, UserProfiles } from '@/models/index.js'; -import { instanceChart, perUserFollowingChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '../create-notification.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { Packed } from '@/misc/schema.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { webhookDeliver } from '@/queue/index.js'; - -const logger = new Logger('following/create'); - -export async function insertFollowingDoc(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }) { - if (follower.id === followee.id) return; - - let alreadyFollowed = false; - - await Followings.insert({ - id: genId(), - createdAt: new Date(), - followerId: follower.id, - followeeId: followee.id, - - // 非正規化 - followerHost: follower.host, - followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null, - followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null, - followeeHost: followee.host, - followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null, - followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null, - }).catch(e => { - if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); - alreadyFollowed = true; - } else { - throw e; - } - }); - - const req = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (req) { - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - // 通知を作成 - createNotification(follower.id, 'followRequestAccepted', { - notifierId: followee.id, - }); - } - - if (alreadyFollowed) return; - - //#region Increment counts - await Promise.all([ - Users.increment({ id: follower.id }, 'followingCount', 1), - Users.increment({ id: followee.id }, 'followersCount', 1), - ]); - //#endregion - - //#region Update instance stats - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - registerOrFetchInstanceDoc(follower.host).then(i => { - Instances.increment({ id: i.id }, 'followingCount', 1); - instanceChart.updateFollowing(i.host, true); - }); - } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - registerOrFetchInstanceDoc(followee.host).then(i => { - Instances.increment({ id: i.id }, 'followersCount', 1); - instanceChart.updateFollowers(i.host, true); - }); - } - //#endregion - - perUserFollowingChart.update(follower, followee, true); - - // Publish follow event - if (Users.isLocalUser(follower)) { - Users.pack(followee.id, follower, { - detail: true, - }).then(async packed => { - publishUserEvent(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">); - publishMainStream(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'follow', { - user: packed, - }); - } - }); - } - - // Publish followed event - if (Users.isLocalUser(followee)) { - Users.pack(follower.id, followee).then(async packed => { - publishMainStream(followee.id, 'followed', packed); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'followed', { - user: packed, - }); - } - }); - - // 通知を作成 - createNotification(followee.id, 'follow', { - notifierId: follower.id, - }); - } -} - -export default async function(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string) { - const [follower, followee] = await Promise.all([ - Users.findOneByOrFail({ id: _follower.id }), - Users.findOneByOrFail({ id: _followee.id }), - ]); - - // check blocking - const [blocking, blocked] = await Promise.all([ - Blockings.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - Blockings.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), - ]); - - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) { - // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 - const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee)); - deliver(followee , content, follower.inbox); - return; - } else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) { - // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 - await Blockings.delete(blocking.id); - } else { - // それ以外は単純に例外 - if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); - if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); - } - - const followeeProfile = await UserProfiles.findOneByOrFail({ userId: followee.id }); - - // フォロー対象が鍵アカウントである or - // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or - // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである - // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { - let autoAccept = false; - - // 鍵アカウントであっても、既にフォローされていた場合はスルー - const following = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - if (following) { - autoAccept = true; - } - - // フォローしているユーザーは自動承認オプション - if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const followed = await Followings.findOneBy({ - followerId: followee.id, - followeeId: follower.id, - }); - - if (followed) autoAccept = true; - } - - if (!autoAccept) { - await createFollowRequest(follower, followee, requestId); - return; - } - } - - await insertFollowingDoc(followee, follower); - - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee)); - deliver(followee, content, follower.inbox); - } -} diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts deleted file mode 100644 index 91b5a3d61d..0000000000 --- a/packages/backend/src/services/following/delete.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { deliver, webhookDeliver } from '@/queue/index.js'; -import Logger from '../logger.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { User } from '@/models/entities/user.js'; -import { Followings, Users, Instances } from '@/models/index.js'; -import { instanceChart, perUserFollowingChart } from '@/services/chart/index.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; - -const logger = new Logger('following/delete'); - -export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, silent = false) { - const following = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (following == null) { - logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); - return; - } - - await Followings.delete(following.id); - - decrementFollowing(follower, followee); - - // Publish unfollow event - if (!silent && Users.isLocalUser(follower)) { - Users.pack(followee.id, follower, { - detail: true, - }).then(async packed => { - publishUserEvent(follower.id, 'unfollow', packed); - publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } - }); - } - - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - deliver(follower, content, followee.inbox); - } - - if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) { - // local user has null host - const content = renderActivity(renderReject(renderFollow(follower, followee), followee)); - deliver(followee, content, follower.inbox); - } -} - -export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { - //#region Decrement following / followers counts - await Promise.all([ - Users.decrement({ id: follower.id }, 'followingCount', 1), - Users.decrement({ id: followee.id }, 'followersCount', 1), - ]); - //#endregion - - //#region Update instance stats - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - registerOrFetchInstanceDoc(follower.host).then(i => { - Instances.decrement({ id: i.id }, 'followingCount', 1); - instanceChart.updateFollowing(i.host, false); - }); - } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - registerOrFetchInstanceDoc(followee.host).then(i => { - Instances.decrement({ id: i.id }, 'followersCount', 1); - instanceChart.updateFollowers(i.host, false); - }); - } - //#endregion - - perUserFollowingChart.update(follower, followee, false); -} diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts deleted file mode 100644 index 691fca2456..0000000000 --- a/packages/backend/src/services/following/reject.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { deliver, webhookDeliver } from '@/queue/index.js'; -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Users, FollowRequests, Followings } from '@/models/index.js'; -import { decrementFollowing } from './delete.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; - -type Local = ILocalUser | { - id: ILocalUser['id']; - host: ILocalUser['host']; - uri: ILocalUser['uri'] -}; -type Remote = IRemoteUser | { - id: IRemoteUser['id']; - host: IRemoteUser['host']; - uri: IRemoteUser['uri']; - inbox: IRemoteUser['inbox']; -}; -type Both = Local | Remote; - -/** - * API following/request/reject - */ -export async function rejectFollowRequest(user: Local, follower: Both) { - if (Users.isRemoteUser(follower)) { - deliverReject(user, follower); - } - - await removeFollowRequest(user, follower); - - if (Users.isLocalUser(follower)) { - publishUnfollow(user, follower); - } -} - -/** - * API following/reject - */ -export async function rejectFollow(user: Local, follower: Both) { - if (Users.isRemoteUser(follower)) { - deliverReject(user, follower); - } - - await removeFollow(user, follower); - - if (Users.isLocalUser(follower)) { - publishUnfollow(user, follower); - } -} - -/** - * AP Reject/Follow - */ -export async function remoteReject(actor: Remote, follower: Local) { - await removeFollowRequest(actor, follower); - await removeFollow(actor, follower); - publishUnfollow(actor, follower); -} - -/** - * Remove follow request record - */ -async function removeFollowRequest(followee: Both, follower: Both) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (!request) return; - - await FollowRequests.delete(request.id); -} - -/** - * Remove follow record - */ -async function removeFollow(followee: Both, follower: Both) { - const following = await Followings.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (!following) return; - - await Followings.delete(following.id); - decrementFollowing(follower, followee); -} - -/** - * Deliver Reject to remote - */ -async function deliverReject(followee: Local, follower: Remote) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee)); - deliver(followee, content, follower.inbox); -} - -/** - * Publish unfollow to local - */ -async function publishUnfollow(followee: Both, follower: Local) { - const packedFollowee = await Users.pack(followee.id, follower, { - detail: true, - }); - - publishUserEvent(follower.id, 'unfollow', packedFollowee); - publishMainStream(follower.id, 'unfollow', packedFollowee); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'unfollow', { - user: packedFollowee, - }); - } -} diff --git a/packages/backend/src/services/following/requests/accept-all.ts b/packages/backend/src/services/following/requests/accept-all.ts deleted file mode 100644 index 5fbb549e01..0000000000 --- a/packages/backend/src/services/following/requests/accept-all.ts +++ /dev/null @@ -1,18 +0,0 @@ -import accept from './accept.js'; -import { User } from '@/models/entities/user.js'; -import { FollowRequests, Users } from '@/models/index.js'; - -/** - * 指定したユーザー宛てのフォローリクエストをすべて承認 - * @param user ユーザー - */ -export default async function(user: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }) { - const requests = await FollowRequests.findBy({ - followeeId: user.id, - }); - - for (const request of requests) { - const follower = await Users.findOneByOrFail({ id: request.followerId }); - accept(user, follower); - } -} diff --git a/packages/backend/src/services/following/requests/accept.ts b/packages/backend/src/services/following/requests/accept.ts deleted file mode 100644 index 20829f70c7..0000000000 --- a/packages/backend/src/services/following/requests/accept.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderAccept from '@/remote/activitypub/renderer/accept.js'; -import { deliver } from '@/queue/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { insertFollowingDoc } from '../create.js'; -import { User, ILocalUser, CacheableUser } from '@/models/entities/user.js'; -import { FollowRequests, Users } from '@/models/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; - -export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, follower: CacheableUser) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (request == null) { - throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); - } - - await insertFollowingDoc(followee, follower); - - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee)); - deliver(followee, content, follower.inbox); - } - - Users.pack(followee.id, followee, { - detail: true, - }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); -} diff --git a/packages/backend/src/services/following/requests/cancel.ts b/packages/backend/src/services/following/requests/cancel.ts deleted file mode 100644 index 56531fa1fd..0000000000 --- a/packages/backend/src/services/following/requests/cancel.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { deliver } from '@/queue/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User, ILocalUser } from '@/models/entities/user.js'; -import { Users, FollowRequests } from '@/models/index.js'; - -export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host'] }) { - if (Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - - if (Users.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので - deliver(follower, content, followee.inbox); - } - } - - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (request == null) { - throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); - } - - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - Users.pack(followee.id, followee, { - detail: true, - }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); -} diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts deleted file mode 100644 index bda2f8f92d..0000000000 --- a/packages/backend/src/services/following/requests/create.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import { deliver } from '@/queue/index.js'; -import { User } from '@/models/entities/user.js'; -import { Blockings, FollowRequests, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '../../create-notification.js'; - -export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, requestId?: string) { - if (follower.id === followee.id) return; - - // check blocking - const [blocking, blocked] = await Promise.all([ - Blockings.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - Blockings.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), - ]); - - if (blocking != null) throw new Error('blocking'); - if (blocked != null) throw new Error('blocked'); - - const followRequest = await FollowRequests.insert({ - id: genId(), - createdAt: new Date(), - followerId: follower.id, - followeeId: followee.id, - requestId, - - // 非正規化 - followerHost: follower.host, - followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined, - followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined, - followeeHost: followee.host, - followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined, - followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined, - }).then(x => FollowRequests.findOneByOrFail(x.identifiers[0])); - - // Publish receiveRequest event - if (Users.isLocalUser(followee)) { - Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed)); - - Users.pack(followee.id, followee, { - detail: true, - }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); - - // 通知を作成 - createNotification(followee.id, 'receiveFollowRequest', { - notifierId: follower.id, - followRequestId: followRequest.id, - }); - } - - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderFollow(follower, followee)); - deliver(follower, content, followee.inbox); - } -} diff --git a/packages/backend/src/services/i/pin.ts b/packages/backend/src/services/i/pin.ts deleted file mode 100644 index f35392a34b..0000000000 --- a/packages/backend/src/services/i/pin.ts +++ /dev/null @@ -1,92 +0,0 @@ -import config from '@/config/index.js'; -import renderAdd from '@/remote/activitypub/renderer/add.js'; -import renderRemove from '@/remote/activitypub/renderer/remove.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes, UserNotePinings, Users } from '@/models/index.js'; -import { UserNotePining } from '@/models/entities/user-note-pining.js'; -import { genId } from '@/misc/gen-id.js'; -import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js'; -import { deliverToRelays } from '../relay.js'; - -/** - * 指定した投稿をピン留めします - * @param user - * @param noteId - */ -export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { - // Fetch pinee - const note = await Notes.findOneBy({ - id: noteId, - userId: user.id, - }); - - if (note == null) { - throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); - } - - const pinings = await UserNotePinings.findBy({ userId: user.id }); - - if (pinings.length >= 5) { - throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); - } - - if (pinings.some(pining => pining.noteId === note.id)) { - throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); - } - - await UserNotePinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - noteId: note.id, - } as UserNotePining); - - // Deliver to remote followers - if (Users.isLocalUser(user)) { - deliverPinnedChange(user.id, note.id, true); - } -} - -/** - * 指定した投稿のピン留めを解除します - * @param user - * @param noteId - */ -export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { - // Fetch unpinee - const note = await Notes.findOneBy({ - id: noteId, - userId: user.id, - }); - - if (note == null) { - throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); - } - - UserNotePinings.delete({ - userId: user.id, - noteId: note.id, - }); - - // Deliver to remote followers - if (Users.isLocalUser(user)) { - deliverPinnedChange(user.id, noteId, false); - } -} - -export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { - const user = await Users.findOneBy({ id: userId }); - if (user == null) throw new Error('user not found'); - - if (!Users.isLocalUser(user)) return; - - const target = `${config.url}/users/${user.id}/collections/featured`; - const item = `${config.url}/notes/${noteId}`; - const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item)); - - deliverToFollowers(user, content); - deliverToRelays(user, content); -} diff --git a/packages/backend/src/services/i/update.ts b/packages/backend/src/services/i/update.ts deleted file mode 100644 index 27bd38bd39..0000000000 --- a/packages/backend/src/services/i/update.ts +++ /dev/null @@ -1,19 +0,0 @@ -import renderUpdate from '@/remote/activitypub/renderer/update.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { renderPerson } from '@/remote/activitypub/renderer/person.js'; -import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js'; -import { deliverToRelays } from '../relay.js'; - -export async function publishToFollowers(userId: User['id']) { - const user = await Users.findOneBy({ id: userId }); - if (user == null) throw new Error('user not found'); - - // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (Users.isLocalUser(user)) { - const content = renderActivity(renderUpdate(await renderPerson(user), user)); - deliverToFollowers(user, content); - deliverToRelays(user, content); - } -} diff --git a/packages/backend/src/services/insert-moderation-log.ts b/packages/backend/src/services/insert-moderation-log.ts deleted file mode 100644 index 0a7c472d8d..0000000000 --- a/packages/backend/src/services/insert-moderation-log.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ModerationLogs } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { User } from '@/models/entities/user.js'; - -export async function insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record) { - await ModerationLogs.insert({ - id: genId(), - createdAt: new Date(), - userId: moderator.id, - type: type, - info: info || {}, - }); -} diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts deleted file mode 100644 index bddd0355a2..0000000000 --- a/packages/backend/src/services/instance-actor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSystemUser } from './create-system-user.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; -import { IsNull } from 'typeorm'; - -const ACTOR_USERNAME = 'instance.actor' as const; - -const cache = new Cache(Infinity); - -export async function getInstanceActor(): Promise { - const cached = cache.get(null); - if (cached) return cached; - - const user = await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }) as ILocalUser | undefined; - - if (user) { - cache.set(null, user); - return user; - } else { - const created = await createSystemUser(ACTOR_USERNAME) as ILocalUser; - cache.set(null, created); - return created; - } -} diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts deleted file mode 100644 index 89d6d57209..0000000000 --- a/packages/backend/src/services/logger.ts +++ /dev/null @@ -1,128 +0,0 @@ -import cluster from 'node:cluster'; -import chalk from 'chalk'; -import { default as convertColor } from 'color-convert'; -import { format as dateFormat } from 'date-fns'; -import { envOption } from '../env.js'; -import config from '@/config/index.js'; - -import * as SyslogPro from 'syslog-pro'; - -type Domain = { - name: string; - color?: string; -}; - -type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; - -export default class Logger { - private domain: Domain; - private parentLogger: Logger | null = null; - private store: boolean; - private syslogClient: any | null = null; - - constructor(domain: string, color?: string, store = true) { - this.domain = { - name: domain, - color: color, - }; - this.store = store; - - if (config.syslog) { - this.syslogClient = new SyslogPro.RFC5424({ - applacationName: 'Misskey', - timestamp: true, - encludeStructuredData: true, - color: true, - extendedColor: true, - server: { - target: config.syslog.host, - port: config.syslog.port, - }, - }); - } - } - - public createSubLogger(domain: string, color?: string, store = true): Logger { - const logger = new Logger(domain, color, store); - logger.parentLogger = this; - return logger; - } - - 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; - if (level === 'debug') store = false; - - if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store); - return; - } - - const time = dateFormat(new Date(), 'HH:mm:ss'); - const worker = cluster.isPrimary ? '*' : cluster.worker.id; - const l = - level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : - level === 'warning' ? chalk.yellow('WARN') : - level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') : - level === 'debug' ? chalk.gray('VERB') : - level === 'info' ? chalk.blue('INFO') : - null; - const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.rgb(...convertColor.keyword.rgb(d.color))(d.name) : chalk.white(d.name)); - const m = - level === 'error' ? chalk.red(message) : - level === 'warning' ? chalk.yellow(message) : - level === 'success' ? chalk.green(message) : - level === 'debug' ? chalk.gray(message) : - level === 'info' ? message : - null; - - let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`; - if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; - - console.log(important ? chalk.bold(log) : log); - - if (store) { - if (this.syslogClient) { - const send = - level === 'error' ? this.syslogClient.error : - level === 'warning' ? this.syslogClient.warning : - level === 'success' ? this.syslogClient.info : - level === 'debug' ? this.syslogClient.info : - level === 'info' ? this.syslogClient.info : - null as never; - - send.bind(this.syslogClient)(message).catch(() => {}); - } - } - } - - public error(x: string | Error, data?: Record | null, important = false): void { // 実行を継続できない状況で使う - if (x instanceof Error) { - data = data || {}; - data.e = x; - this.log('error', x.toString(), data, important); - } else if (typeof x === 'object') { - this.log('error', `${(x as any).message || (x as any).name || x}`, data, important); - } else { - this.log('error', `${x}`, data, important); - } - } - - public warn(message: string, data?: Record | null, important = false): void { // 実行を継続できるが改善すべき状況で使う - this.log('warning', message, data, important); - } - - public succ(message: string, data?: Record | null, important = false): void { // 何かに成功した状況で使う - this.log('success', message, data, important); - } - - public debug(message: string, data?: Record | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) - if (process.env.NODE_ENV !== 'production' || envOption.verbose) { - this.log('debug', message, data, important); - } - } - - public info(message: string, data?: Record | null, important = false): void { // それ以外 - this.log('info', message, data, important); - } -} diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts deleted file mode 100644 index e6b3204922..0000000000 --- a/packages/backend/src/services/messages/create.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { CacheableUser, User } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { Not } from 'typeorm'; -import { Note } from '@/models/entities/note.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import renderCreate from '@/remote/activitypub/renderer/create.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { deliver } from '@/queue/index.js'; - -export async function 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: genId(), - createdAt: new Date(), - fileId: file ? file.id : null, - recipientId: recipientUser ? recipientUser.id : null, - groupId: recipientGroup ? recipientGroup.id : null, - text: text ? text.trim() : null, - userId: user.id, - isRead: false, - reads: [] as any[], - uri, - } as MessagingMessage; - - await MessagingMessages.insert(message); - - const messageObj = await MessagingMessages.pack(message); - - if (recipientUser) { - if (Users.isLocalUser(user)) { - // 自分のストリーム - publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); - publishMessagingIndexStream(message.userId, 'message', messageObj); - publishMainStream(message.userId, 'messagingMessage', messageObj); - } - - if (Users.isLocalUser(recipientUser)) { - // 相手のストリーム - publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); - publishMessagingIndexStream(recipientUser.id, 'message', messageObj); - publishMainStream(recipientUser.id, 'messagingMessage', messageObj); - } - } else if (recipientGroup) { - // グループのストリーム - publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); - - // メンバーのストリーム - const joinings = await UserGroupJoinings.findBy({ userGroupId: recipientGroup.id }); - for (const joining of joinings) { - publishMessagingIndexStream(joining.userId, 'message', messageObj); - publishMainStream(joining.userId, 'messagingMessage', messageObj); - } - } - - // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する - setTimeout(async () => { - const freshMessage = await MessagingMessages.findOneBy({ id: message.id }); - if (freshMessage == null) return; // メッセージが削除されている場合もある - - if (recipientUser && Users.isLocalUser(recipientUser)) { - if (freshMessage.isRead) return; // 既読 - - //#region ただしミュートされているなら発行しない - const mute = await Mutings.findBy({ - muterId: recipientUser.id, - }); - if (mute.map(m => m.muteeId).includes(user.id)) return; - //#endregion - - publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); - pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj); - } else if (recipientGroup) { - const joinings = await UserGroupJoinings.findBy({ userGroupId: recipientGroup.id, userId: Not(user.id) }); - for (const joining of joinings) { - if (freshMessage.reads.includes(joining.userId)) return; // 既読 - publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); - pushNotification(joining.userId, 'unreadMessagingMessage', messageObj); - } - } - }, 2000); - - if (recipientUser && Users.isLocalUser(user) && Users.isRemoteUser(recipientUser)) { - const note = { - id: message.id, - createdAt: message.createdAt, - fileIds: message.fileId ? [ message.fileId ] : [], - text: message.text, - userId: message.userId, - visibility: 'specified', - mentions: [ recipientUser ].map(u => u.id), - mentionedRemoteUsers: JSON.stringify([ recipientUser ].map(u => ({ - uri: u.uri, - username: u.username, - host: u.host, - }))), - } as Note; - - const activity = renderActivity(renderCreate(await renderNote(note, false, true), note)); - - deliver(user, activity, recipientUser.inbox); - } - return messageObj; -} diff --git a/packages/backend/src/services/messages/delete.ts b/packages/backend/src/services/messages/delete.ts deleted file mode 100644 index 1e7ce1981c..0000000000 --- a/packages/backend/src/services/messages/delete.ts +++ /dev/null @@ -1,30 +0,0 @@ -import config from '@/config/index.js'; -import { MessagingMessages, Users } from '@/models/index.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderDelete from '@/remote/activitypub/renderer/delete.js'; -import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; -import { deliver } from '@/queue/index.js'; - -export async function deleteMessage(message: MessagingMessage) { - await MessagingMessages.delete(message.id); - postDeleteMessage(message); -} - -async function postDeleteMessage(message: MessagingMessage) { - if (message.recipientId) { - const user = await Users.findOneByOrFail({ id: message.userId }); - const recipient = await Users.findOneByOrFail({ id: message.recipientId }); - - if (Users.isLocalUser(user)) publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); - if (Users.isLocalUser(recipient)) publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); - - if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) { - const activity = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${message.id}`), user)); - deliver(user, activity, recipient.inbox); - } - } else if (message.groupId) { - publishGroupMessagingStream(message.groupId, 'deleted', message.id); - } -} diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts deleted file mode 100644 index e2bf9d5b59..0000000000 --- a/packages/backend/src/services/note/create.ts +++ /dev/null @@ -1,692 +0,0 @@ -import * as mfm from 'mfm-js'; -import es from '../../db/elasticsearch.js'; -import { publishMainStream, publishNotesStream } from '@/services/stream.js'; -import DeliverManager from '@/remote/activitypub/deliver-manager.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import renderCreate from '@/remote/activitypub/renderer/create.js'; -import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import config from '@/config/index.js'; -import { updateHashtags } from '../update-hashtag.js'; -import { concat } from '@/prelude/array.js'; -import { insertNoteUnread } from '@/services/note/unread.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { extractMentions } from '@/misc/extract-mentions.js'; -import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; -import { extractHashtags } from '@/misc/extract-hashtags.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; -import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { App } from '@/models/entities/app.js'; -import { Not, In } from 'typeorm'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { genId } from '@/misc/gen-id.js'; -import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index.js'; -import { Poll, IPoll } from '@/models/entities/poll.js'; -import { createNotification } from '../create-notification.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkHitAntenna } from '@/misc/check-hit-antenna.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { addNoteToAntenna } from '../add-note-to-antenna.js'; -import { countSameRenotes } from '@/misc/count-same-renotes.js'; -import { deliverToRelays } from '../relay.js'; -import { Channel } from '@/models/entities/channel.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { getAntennas } from '@/misc/antenna-cache.js'; -import { endedPollNotificationQueue } from '@/queue/queues.js'; -import { webhookDeliver } from '@/queue/index.js'; -import { Cache } from '@/misc/cache.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { db } from '@/db/postgre.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; - -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); - -type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; - -class NotificationManager { - private notifier: { id: User['id']; }; - private note: Note; - private queue: { - target: ILocalUser['id']; - reason: NotificationType; - }[]; - - constructor(notifier: { id: User['id']; }, note: Note) { - this.notifier = notifier; - this.note = note; - this.queue = []; - } - - public push(notifiee: ILocalUser['id'], reason: NotificationType) { - // 自分自身へは通知しない - if (this.notifier.id === notifiee) return; - - const exist = this.queue.find(x => x.target === notifiee); - - if (exist) { - // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする - if (reason !== 'mention') { - exist.reason = reason; - } - } else { - this.queue.push({ - reason: reason, - target: notifiee, - }); - } - } - - public async deliver() { - for (const x of this.queue) { - // ミュート情報を取得 - const mentioneeMutes = await Mutings.findBy({ - muterId: x.target, - }); - - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); - - // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する - if (!mentioneesMutedUserIds.includes(this.notifier.id)) { - createNotification(x.target, x.reason, { - notifierId: this.notifier.id, - noteId: this.note.id, - }); - } - } - } -} - -type MinimumUser = { - id: User['id']; - host: User['host']; - username: User['username']; - uri: User['uri']; -}; - -type Option = { - createdAt?: Date | null; - name?: string | null; - text?: string | null; - reply?: Note | null; - renote?: Note | null; - files?: DriveFile[] | null; - poll?: IPoll | null; - localOnly?: boolean | null; - cw?: string | null; - visibility?: string; - visibleUsers?: MinimumUser[] | null; - channel?: Channel | null; - apMentions?: MinimumUser[] | null; - apHashtags?: string[] | null; - apEmojis?: string[] | null; - uri?: string | null; - url?: string | null; - app?: App | null; -}; - -export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise(async (res, rej) => { - // チャンネル外にリプライしたら対象のスコープに合わせる - // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) - if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { - if (data.reply.channelId) { - data.channel = await Channels.findOneBy({ id: data.reply.channelId }); - } else { - data.channel = null; - } - } - - // チャンネル内にリプライしたら対象のスコープに合わせる - // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) - if (data.reply && (data.channel == null) && data.reply.channelId) { - data.channel = await Channels.findOneBy({ id: data.reply.channelId }); - } - - if (data.createdAt == null) data.createdAt = new Date(); - if (data.visibility == null) data.visibility = 'public'; - if (data.localOnly == null) data.localOnly = false; - if (data.channel != null) data.visibility = 'public'; - 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'; - } - - // Renote対象が「ホームまたは全体」以外の公開範囲ならreject - if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { - return rej('Renote target is not public or home'); - } - - // Renote対象がpublicではないならhomeにする - if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { - data.visibility = 'home'; - } - - // Renote対象がfollowersならfollowersにする - if (data.renote && data.renote.visibility === 'followers') { - data.visibility = 'followers'; - } - - // 返信対象がpublicではないならhomeにする - if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { - data.visibility = 'home'; - } - - // ローカルのみをRenoteしたらローカルのみにする - if (data.renote && data.renote.localOnly && data.channel == null) { - data.localOnly = true; - } - - // ローカルのみにリプライしたらローカルのみにする - if (data.reply && data.reply.localOnly && data.channel == null) { - data.localOnly = true; - } - - if (data.text) { - data.text = data.text.trim(); - } else { - data.text = null; - } - - let tags = data.apHashtags; - let emojis = data.apEmojis; - let mentionedUsers = data.apMentions; - - // Parse MFM if needed - if (!tags || !emojis || !mentionedUsers) { - const tokens = data.text ? mfm.parse(data.text)! : []; - const cwTokens = data.cw ? mfm.parse(data.cw)! : []; - const choiceTokens = data.poll && data.poll.choices - ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) - : []; - - const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); - - tags = data.apHashtags || extractHashtags(combinedTokens); - - emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens); - - mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); - } - - tags = tags.filter(tag => Array.from(tag || '').length <= 128).splice(0, 32); - - if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { - mentionedUsers.push(await Users.findOneByOrFail({ id: data.reply!.userId })); - } - - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - if (!mentionedUsers.some(x => x.id === u.id)) { - mentionedUsers.push(u); - } - } - - if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { - data.visibleUsers.push(await Users.findOneByOrFail({ id: data.reply!.userId })); - } - } - - const note = await insertNote(user, data, tags, emojis, mentionedUsers); - - res(note); - - // 統計を更新 - notesChart.update(note, true); - perUserNotesChart.update(user, note, true); - - // Register host - if (Users.isRemoteUser(user)) { - registerOrFetchInstanceDoc(user.host).then(i => { - Instances.increment({ id: i.id }, 'notesCount', 1); - instanceChart.updateNote(i.host, note, true); - }); - } - - // ハッシュタグ更新 - if (data.visibility === 'public' || data.visibility === 'home') { - updateHashtags(user, tags); - } - - // Increment notes count (user) - incNotesCountOfUser(user); - - // Word mute - mutedWordsCache.fetch(null, () => UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - })).then(us => { - for (const u of us) { - checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { - if (shouldMute) { - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: 'word', - }); - } - }); - } - }); - - // Antenna - for (const antenna of (await getAntennas())) { - checkHitAntenna(antenna, note, user).then(hit => { - if (hit) { - addNoteToAntenna(antenna, note, user); - } - }); - } - - // Channel - if (note.channelId) { - ChannelFollowings.findBy({ followeeId: note.channelId }).then(followings => { - for (const following of followings) { - insertNoteUnread(following.followerId, note, { - isSpecified: false, - isMentioned: false, - }); - } - }); - } - - if (data.reply) { - saveReply(data.reply, note); - } - - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき - if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) { - incRenoteCount(data.renote); - } - - if (data.poll && data.poll.expiresAt) { - const delay = data.poll.expiresAt.getTime() - Date.now(); - endedPollNotificationQueue.add({ - noteId: note.id, - }, { - delay, - removeOnComplete: true, - }); - } - - if (!silent) { - if (Users.isLocalUser(user)) activeUsersChart.write(user); - - // 未読通知を作成 - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - // ローカルユーザーのみ - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: true, - isMentioned: false, - }); - } - } else { - for (const u of mentionedUsers) { - // ローカルユーザーのみ - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: false, - isMentioned: true, - }); - } - } - - // Pack the note - const noteObj = await Notes.pack(note); - - publishNotesStream(noteObj); - - getActiveWebhooks().then(webhooks => { - webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'note', { - note: noteObj, - }); - } - }); - - const nm = new NotificationManager(user, note); - const nmRelatedPromises = []; - - await createMentionedEvents(mentionedUsers, note, nm); - - // If has in reply to note - if (data.reply) { - // Fetch watchers - nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); - - // 通知 - if (data.reply.userHost === null) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: data.reply.userId, - threadId: data.reply.threadId || data.reply.id, - }); - - if (!threadMuted) { - nm.push(data.reply.userId, 'reply'); - publishMainStream(data.reply.userId, 'reply', noteObj); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'reply', { - note: noteObj, - }); - } - } - } - } - - // If it is renote - if (data.renote) { - const type = data.text ? 'quote' : 'renote'; - - // Notify - if (data.renote.userHost === null) { - nm.push(data.renote.userId, type); - } - - // Fetch watchers - nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); - - // Publish event - if ((user.id !== data.renote.userId) && data.renote.userHost === null) { - publishMainStream(data.renote.userId, 'renote', noteObj); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'renote', { - note: noteObj, - }); - } - } - } - - Promise.all(nmRelatedPromises).then(() => { - nm.deliver(); - }); - - //#region AP deliver - if (Users.isLocalUser(user)) { - (async () => { - const noteActivity = await renderNoteOrRenoteActivity(data, note); - const dm = new DeliverManager(user, noteActivity); - - // メンションされたリモートユーザーに配送 - for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); - } - - // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 - if (data.reply && data.reply.userHost !== null) { - const u = await Users.findOneBy({ id: data.reply.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 - if (data.renote && data.renote.userHost !== null) { - const u = await Users.findOneBy({ id: data.renote.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // フォロワーに配送 - if (['public', 'home', 'followers'].includes(note.visibility)) { - dm.addFollowersRecipe(); - } - - if (['public'].includes(note.visibility)) { - deliverToRelays(user, noteActivity); - } - - dm.execute(); - })(); - } - //#endregion - } - - if (data.channel) { - Channels.increment({ id: data.channel.id }, 'notesCount', 1); - Channels.update(data.channel.id, { - lastNotedAt: new Date(), - }); - - Notes.countBy({ - userId: user.id, - channelId: data.channel.id, - }).then(count => { - // この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる - // TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい - if (count === 1) { - Channels.increment({ id: data.channel!.id }, 'usersCount', 1); - } - }); - } - - // Register to search database - index(note); -}); - -async function renderNoteOrRenoteActivity(data: Option, note: Note) { - if (data.localOnly) return null; - - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) - ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note) - : renderCreate(await renderNote(note, false), note); - - return renderActivity(content); -} - -function incRenoteCount(renote: Note) { - Notes.createQueryBuilder().update() - .set({ - renoteCount: () => '"renoteCount" + 1', - score: () => '"score" + 1', - }) - .where('id = :id', { id: renote.id }) - .execute(); -} - -async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { - const insert = new Note({ - id: genId(data.createdAt!), - createdAt: data.createdAt!, - fileIds: data.files ? data.files.map(file => file.id) : [], - replyId: data.reply ? data.reply.id : null, - renoteId: data.renote ? data.renote.id : null, - channelId: data.channel ? data.channel.id : null, - threadId: data.reply - ? data.reply.threadId - ? data.reply.threadId - : data.reply.id - : null, - name: data.name, - text: data.text, - hasPoll: data.poll != null, - cw: data.cw == null ? null : data.cw, - tags: tags.map(tag => normalizeForSearch(tag)), - emojis, - userId: user.id, - localOnly: data.localOnly!, - visibility: data.visibility as any, - visibleUserIds: data.visibility === 'specified' - ? data.visibleUsers - ? data.visibleUsers.map(u => u.id) - : [] - : [], - - attachedFileTypes: data.files ? data.files.map(file => file.type) : [], - - // 以下非正規化データ - replyUserId: data.reply ? data.reply.userId : null, - replyUserHost: data.reply ? data.reply.userHost : null, - renoteUserId: data.renote ? data.renote.userId : null, - renoteUserHost: data.renote ? data.renote.userHost : null, - userHost: user.host, - }); - - if (data.uri != null) insert.uri = data.uri; - if (data.url != null) insert.url = data.url; - - // Append mentions data - if (mentionedUsers.length > 0) { - insert.mentions = mentionedUsers.map(u => u.id); - const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); - insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => { - const profile = profiles.find(p => p.userId === u.id); - const url = profile != null ? profile.url : null; - return { - uri: u.uri, - url: url == null ? undefined : url, - username: u.username, - host: u.host, - } as IMentionedRemoteUsers[0]; - })); - } - - // 投稿を作成 - try { - if (insert.hasPoll) { - // Start transaction - await db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.insert(Note, insert); - - const poll = new Poll({ - noteId: insert.id, - choices: data.poll!.choices, - expiresAt: data.poll!.expiresAt, - multiple: data.poll!.multiple, - votes: new Array(data.poll!.choices.length).fill(0), - noteVisibility: insert.visibility, - userId: user.id, - userHost: user.host, - }); - - await transactionalEntityManager.insert(Poll, poll); - }); - } else { - await Notes.insert(insert); - } - - return insert; - } catch (e) { - // duplicate key error - if (isDuplicateKeyValueError(e)) { - const err = new Error('Duplicated note'); - err.name = 'duplicated'; - throw err; - } - - console.error(e); - - throw e; - } -} - -function index(note: Note) { - if (note.text == null || config.elasticsearch == null) return; - - es!.index({ - index: config.elasticsearch.index || 'misskey_note', - id: note.id.toString(), - body: { - text: normalizeForSearch(note.text), - userId: note.userId, - userHost: note.userHost, - }, - }); -} - -async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType) { - const watchers = await NoteWatchings.findBy({ - noteId: renote.id, - userId: Not(user.id), - }); - - for (const watcher of watchers) { - nm.push(watcher.userId, type); - } -} - -async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager) { - const watchers = await NoteWatchings.findBy({ - noteId: reply.id, - userId: Not(user.id), - }); - - for (const watcher of watchers) { - nm.push(watcher.userId, 'reply'); - } -} - -async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { - for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: u.id, - threadId: note.threadId || note.id, - }); - - if (threadMuted) { - continue; - } - - const detailPackedNote = await Notes.pack(note, u, { - detail: true, - }); - - publishMainStream(u.id, 'mention', detailPackedNote); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'mention', { - note: detailPackedNote, - }); - } - - // Create notification - nm.push(u.id, 'mention'); - } -} - -function saveReply(reply: Note, note: Note) { - Notes.increment({ id: reply.id }, 'repliesCount', 1); -} - -function incNotesCountOfUser(user: { id: User['id']; }) { - Users.createQueryBuilder().update() - .set({ - updatedAt: new Date(), - notesCount: () => '"notesCount" + 1', - }) - .where('id = :id', { id: user.id }) - .execute(); -} - -async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { - if (tokens == null) return []; - - const mentions = extractMentions(tokens); - - let mentionedUsers = (await Promise.all(mentions.map(m => - resolveUser(m.username, m.host || user.host).catch(() => null) - ))).filter(x => x != null) as User[]; - - // Drop duplicate users - mentionedUsers = mentionedUsers.filter((u, i, self) => - i === self.findIndex(u2 => u.id === u2.id) - ); - - return mentionedUsers; -} diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts deleted file mode 100644 index 4963200161..0000000000 --- a/packages/backend/src/services/note/delete.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Brackets, In } from 'typeorm'; -import { publishNoteStream } from '@/services/stream.js'; -import renderDelete from '@/remote/activitypub/renderer/delete.js'; -import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; -import config from '@/config/index.js'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; -import { Notes, Users, Instances } from '@/models/index.js'; -import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; -import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js'; -import { countSameRenotes } from '@/misc/count-same-renotes.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { deliverToRelays } from '../relay.js'; - -/** - * 投稿を削除します。 - * @param user 投稿者 - * @param note 投稿 - */ -export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false) { - const deletedAt = new Date(); - - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき - if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) { - Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); - Notes.decrement({ id: note.renoteId }, 'score', 1); - } - - if (note.replyId) { - await Notes.decrement({ id: note.replyId }, 'repliesCount', 1); - } - - if (!quiet) { - publishNoteStream(note.id, 'deleted', { - deletedAt: deletedAt, - }); - - //#region ローカルの投稿なら削除アクティビティを配送 - if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; - - // if deletd note is renote - if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { - renote = await Notes.findOneBy({ - id: note.renoteId, - }); - } - - const content = renderActivity(renote - ? renderUndo(renderAnnounce(renote.uri || `${config.url}/notes/${renote.id}`, note), user) - : renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); - - deliverToConcerned(user, note, content); - } - - // also deliever delete activity to cascaded notes - const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes - for (const cascadingNote of cascadingNotes) { - if (!cascadingNote.user) continue; - if (!Users.isLocalUser(cascadingNote.user)) continue; - const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); - deliverToConcerned(cascadingNote.user, cascadingNote, content); - } - //#endregion - - // 統計を更新 - notesChart.update(note, false); - perUserNotesChart.update(user, note, false); - - if (Users.isRemoteUser(user)) { - registerOrFetchInstanceDoc(user.host).then(i => { - Instances.decrement({ id: i.id }, 'notesCount', 1); - instanceChart.updateNote(i.host, note, false); - }); - } - } - - await Notes.delete({ - id: note.id, - userId: user.id, - }); -} - -async function findCascadingNotes(note: Note) { - const cascadingNotes: Note[] = []; - - const recursive = async (noteId: string) => { - const query = Notes.createQueryBuilder('note') - .where('note.replyId = :noteId', { noteId }) - .orWhere(new Brackets(q => { - q.where('note.renoteId = :noteId', { noteId }) - .andWhere('note.text IS NOT NULL'); - })) - .leftJoinAndSelect('note.user', 'user'); - const replies = await query.getMany(); - for (const reply of replies) { - cascadingNotes.push(reply); - await recursive(reply.id); - } - }; - await recursive(note.id); - - return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users -} - -async function getMentionedRemoteUsers(note: Note) { - const where = [] as any[]; - - // mention / reply / dm - const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - if (uris.length > 0) { - where.push( - { uri: In(uris) }, - ); - } - - // renote / quote - if (note.renoteUserId) { - where.push({ - id: note.renoteUserId, - }); - } - - if (where.length === 0) return []; - - return await Users.find({ - where, - }) as IRemoteUser[]; -} - -async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { - deliverToFollowers(user, content); - deliverToRelays(user, content); - const remoteUsers = await getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - deliverToUser(user, content, remoteUser); - } -} diff --git a/packages/backend/src/services/note/polls/update.ts b/packages/backend/src/services/note/polls/update.ts deleted file mode 100644 index 68cbb9835a..0000000000 --- a/packages/backend/src/services/note/polls/update.ts +++ /dev/null @@ -1,21 +0,0 @@ -import renderUpdate from '@/remote/activitypub/renderer/update.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import { Users, Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js'; -import { deliverToRelays } from '../../relay.js'; - -export async function deliverQuestionUpdate(noteId: Note['id']) { - const note = await Notes.findOneBy({ id: noteId }); - if (note == null) throw new Error('note not found'); - - const user = await Users.findOneBy({ id: note.userId }); - if (user == null) throw new Error('note not found'); - - if (Users.isLocalUser(user)) { - const content = renderActivity(renderUpdate(await renderNote(note, false), user)); - deliverToFollowers(user, content); - deliverToRelays(user, content); - } -} diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts deleted file mode 100644 index 84d98769d9..0000000000 --- a/packages/backend/src/services/note/polls/vote.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { publishNoteStream } from '@/services/stream.js'; -import { CacheableUser, User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index.js'; -import { Not } from 'typeorm'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '../../create-notification.js'; - -export default async function(user: CacheableUser, note: Note, choice: number) { - const poll = await Polls.findOneBy({ noteId: note.id }); - - if (poll == null) throw new Error('poll not found'); - - // Check whether is valid choice - if (poll.choices[choice] == null) throw new Error('invalid choice param'); - - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new Error('blocked'); - } - } - - // if already voted - const exist = await PollVotes.findBy({ - noteId: note.id, - userId: user.id, - }); - - if (poll.multiple) { - if (exist.some(x => x.choice === choice)) { - throw new Error('already voted'); - } - } else if (exist.length !== 0) { - throw new Error('already voted'); - } - - // Create vote - await PollVotes.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - choice: choice, - }); - - // Increment votes count - const index = choice + 1; // In SQL, array index is 1 based - await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - - publishNoteStream(note.id, 'pollVoted', { - choice: choice, - userId: user.id, - }); - - // Notify - createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: choice, - }); - - // Fetch watchers - NoteWatchings.findBy({ - noteId: note.id, - userId: Not(user.id), - }) - .then(watchers => { - for (const watcher of watchers) { - createNotification(watcher.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: choice, - }); - } - }); -} diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts deleted file mode 100644 index 83d302826a..0000000000 --- a/packages/backend/src/services/note/reaction/create.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { publishNoteStream } from '@/services/stream.js'; -import { renderLike } from '@/remote/activitypub/renderer/like.js'; -import DeliverManager from '@/remote/activitypub/deliver-manager.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { toDbReaction, decodeReaction } from '@/misc/reaction-lib.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from '@/models/index.js'; -import { IsNull, Not } from 'typeorm'; -import { perUserReactionsChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '../../create-notification.js'; -import deleteReaction from './delete.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; - -export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => { - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); - } - } - - // check visibility - if (!await Notes.isVisibleForMe(note, user.id)) { - throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); - } - - // TODO: cache - reaction = await toDbReaction(reaction, user.host); - - const record: NoteReaction = { - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - reaction, - }; - - // Create reaction - try { - await NoteReactions.insert(record); - } catch (e) { - if (isDuplicateKeyValueError(e)) { - const exists = await NoteReactions.findOneByOrFail({ - noteId: note.id, - userId: user.id, - }); - - if (exists.reaction !== reaction) { - // 別のリアクションがすでにされていたら置き換える - await deleteReaction(user, note); - await NoteReactions.insert(record); - } else { - // 同じリアクションがすでにされていたらエラー - throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); - } - } else { - throw e; - } - } - - // Increment reactions count - const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; - await Notes.createQueryBuilder().update() - .set({ - reactions: () => sql, - score: () => '"score" + 1', - }) - .where('id = :id', { id: note.id }) - .execute(); - - perUserReactionsChart.update(user, note); - - // カスタム絵文字リアクションだったら絵文字情報も送る - const decodedReaction = decodeReaction(reaction); - - const emoji = await Emojis.findOne({ - where: { - name: decodedReaction.name, - host: decodedReaction.host ?? IsNull(), - }, - select: ['name', 'host', 'originalUrl', 'publicUrl'], - }); - - publishNoteStream(note.id, 'reacted', { - reaction: decodedReaction.reaction, - emoji: emoji != null ? { - name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, - url: emoji.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため - } : null, - userId: user.id, - }); - - // リアクションされたユーザーがローカルユーザーなら通知を作成 - if (note.userHost === null) { - createNotification(note.userId, 'reaction', { - notifierId: user.id, - noteId: note.id, - reaction: reaction, - }); - } - - // Fetch watchers - NoteWatchings.findBy({ - noteId: note.id, - userId: Not(user.id), - }).then(watchers => { - for (const watcher of watchers) { - createNotification(watcher.userId, 'reaction', { - notifierId: user.id, - noteId: note.id, - reaction: reaction, - }); - } - }); - - //#region 配信 - if (Users.isLocalUser(user) && !note.localOnly) { - const content = renderActivity(await renderLike(record, note)); - const dm = new DeliverManager(user, content); - if (note.userHost !== null) { - const reactee = await Users.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); - } - - if (['public', 'home', 'followers'].includes(note.visibility)) { - dm.addFollowersRecipe(); - } else if (note.visibility === 'specified') { - const visibleUsers = await Promise.all(note.visibleUserIds.map(id => Users.findOneBy({ id }))); - for (const u of visibleUsers.filter(u => u && Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); - } - } - - dm.execute(); - } - //#endregion -}; diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts deleted file mode 100644 index a7cbcb1c17..0000000000 --- a/packages/backend/src/services/note/reaction/delete.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { publishNoteStream } from '@/services/stream.js'; -import { renderLike } from '@/remote/activitypub/renderer/like.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import DeliverManager from '@/remote/activitypub/deliver-manager.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReactions, Users, Notes } from '@/models/index.js'; -import { decodeReaction } from '@/misc/reaction-lib.js'; - -export default async (user: { id: User['id']; host: User['host']; }, note: Note) => { - // if already unreacted - const exist = await NoteReactions.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); - } - - // Delete reaction - const result = await NoteReactions.delete(exist.id); - - if (result.affected !== 1) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); - } - - // Decrement reactions count - const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; - await Notes.createQueryBuilder().update() - .set({ - reactions: () => sql, - }) - .where('id = :id', { id: note.id }) - .execute(); - - Notes.decrement({ id: note.id }, 'score', 1); - - publishNoteStream(note.id, 'unreacted', { - reaction: decodeReaction(exist.reaction).reaction, - userId: user.id, - }); - - //#region 配信 - if (Users.isLocalUser(user) && !note.localOnly) { - const content = renderActivity(renderUndo(await renderLike(exist, note), user)); - const dm = new DeliverManager(user, content); - if (note.userHost !== null) { - const reactee = await Users.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); - } - dm.addFollowersRecipe(); - dm.execute(); - } - //#endregion -}; diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts deleted file mode 100644 index 915a9e9eef..0000000000 --- a/packages/backend/src/services/note/read.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { NoteUnreads, AntennaNotes, Users, Followings, ChannelFollowings } from '@/models/index.js'; -import { Not, IsNull, In } from 'typeorm'; -import { Channel } from '@/models/entities/channel.js'; -import { checkHitAntenna } from '@/misc/check-hit-antenna.js'; -import { getAntennas } from '@/misc/antenna-cache.js'; -import { readNotificationByQuery } from '@/server/api/common/read-notification.js'; -import { Packed } from '@/misc/schema.js'; - -/** - * Mark notes as read - */ -export default async function( - userId: User['id'], - notes: (Note | Packed<'Note'>)[], - info?: { - following: Set; - followingChannels: Set; - } -) { - const following = info?.following ? info.following : new Set((await Followings.find({ - where: { - followerId: userId, - }, - select: ['followeeId'], - })).map(x => x.followeeId)); - const followingChannels = info?.followingChannels ? info.followingChannels : new Set((await ChannelFollowings.find({ - where: { - followerId: userId, - }, - select: ['followeeId'], - })).map(x => x.followeeId)); - - const myAntennas = (await getAntennas()).filter(a => a.userId === userId); - const readMentions: (Note | Packed<'Note'>)[] = []; - const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; - const readChannelNotes: (Note | Packed<'Note'>)[] = []; - const readAntennaNotes: (Note | Packed<'Note'>)[] = []; - - for (const note of notes) { - if (note.mentions && note.mentions.includes(userId)) { - readMentions.push(note); - } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { - readSpecifiedNotes.push(note); - } - - if (note.channelId && followingChannels.has(note.channelId)) { - readChannelNotes.push(note); - } - - if (note.user != null) { // たぶんnullになることは無いはずだけど一応 - for (const antenna of myAntennas) { - if (await checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) { - readAntennaNotes.push(note); - } - } - } - } - - if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { - // Remove the record - await NoteUnreads.delete({ - userId: userId, - noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), - }); - - // TODO: ↓まとめてクエリしたい - - NoteUnreads.countBy({ - userId: userId, - isMentioned: true, - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadMentions'); - } - }); - - NoteUnreads.countBy({ - userId: userId, - isSpecified: true, - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - }); - - NoteUnreads.countBy({ - userId: userId, - noteChannelId: Not(IsNull()), - }).then(channelNoteCount => { - if (channelNoteCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllChannels'); - } - }); - - readNotificationByQuery(userId, { - noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), - }); - } - - if (readAntennaNotes.length > 0) { - await AntennaNotes.update({ - antennaId: In(myAntennas.map(a => a.id)), - noteId: In(readAntennaNotes.map(n => n.id)), - }, { - read: true, - }); - - // TODO: まとめてクエリしたい - for (const antenna of myAntennas) { - const count = await AntennaNotes.countBy({ - antennaId: antenna.id, - read: false, - }); - - if (count === 0) { - publishMainStream(userId, 'readAntenna', antenna); - } - } - - Users.getHasUnreadAntenna(userId).then(unread => { - if (!unread) { - publishMainStream(userId, 'readAllAntennas'); - } - }); - } -} diff --git a/packages/backend/src/services/note/unread.ts b/packages/backend/src/services/note/unread.ts deleted file mode 100644 index d9ed711e03..0000000000 --- a/packages/backend/src/services/note/unread.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Note } from '@/models/entities/note.js'; -import { publishMainStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; -import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; - -export async function insertNoteUnread(userId: User['id'], note: Note, params: { - // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse - isSpecified: boolean; - isMentioned: boolean; -}) { - //#region ミュートしているなら無視 - // TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする - const mute = await Mutings.findBy({ - muterId: userId, - }); - if (mute.map(m => m.muteeId).includes(note.userId)) return; - //#endregion - - // スレッドミュート - const threadMute = await NoteThreadMutings.findOneBy({ - userId: userId, - threadId: note.threadId || note.id, - }); - if (threadMute) return; - - const unread = { - id: genId(), - noteId: note.id, - userId: userId, - isSpecified: params.isSpecified, - isMentioned: params.isMentioned, - noteChannelId: note.channelId, - noteUserId: note.userId, - }; - - await NoteUnreads.insert(unread); - - // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する - setTimeout(async () => { - const exist = await NoteUnreads.findOneBy({ id: unread.id }); - - if (exist == null) return; - - if (params.isMentioned) { - publishMainStream(userId, 'unreadMention', note.id); - } - if (params.isSpecified) { - publishMainStream(userId, 'unreadSpecifiedNote', note.id); - } - if (note.channelId) { - publishMainStream(userId, 'unreadChannel', note.id); - } - }, 2000); -} diff --git a/packages/backend/src/services/note/unwatch.ts b/packages/backend/src/services/note/unwatch.ts deleted file mode 100644 index 3964b2ba5f..0000000000 --- a/packages/backend/src/services/note/unwatch.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { NoteWatchings } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; - -export default async (me: User['id'], note: Note) => { - await NoteWatchings.delete({ - noteId: note.id, - userId: me, - }); -}; diff --git a/packages/backend/src/services/note/watch.ts b/packages/backend/src/services/note/watch.ts deleted file mode 100644 index 2210c44a75..0000000000 --- a/packages/backend/src/services/note/watch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteWatchings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { NoteWatching } from '@/models/entities/note-watching.js'; - -export default async (me: User['id'], note: Note) => { - // 自分の投稿はwatchできない - if (me === note.userId) { - return; - } - - await NoteWatchings.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: me, - noteUserId: note.userId, - } as NoteWatching); -}; diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts deleted file mode 100644 index 393a23d050..0000000000 --- a/packages/backend/src/services/push-notification.ts +++ /dev/null @@ -1,85 +0,0 @@ -import push from 'web-push'; -import config from '@/config/index.js'; -import { SwSubscriptions } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Packed } from '@/misc/schema.js'; -import { getNoteSummary } from '@/misc/get-note-summary.js'; - -// Defined also packages/sw/types.ts#L14-L21 -type pushNotificationsTypes = { - 'notification': Packed<'Notification'>; - 'unreadMessagingMessage': Packed<'MessagingMessage'>; - 'readNotifications': { notificationIds: string[] }; - 'readAllNotifications': undefined; - 'readAllMessagingMessages': undefined; - 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; -}; - -// プッシュメッセージサーバーには文字数制限があるため、内容を削減します -function truncateNotification(notification: Packed<'Notification'>): any { - if (notification.note) { - return { - ...notification, - note: { - ...notification.note, - // textをgetNoteSummaryしたものに置き換える - text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), - - cw: undefined, - reply: undefined, - renote: undefined, - user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる - } - }; - } - - return notification; -} - -export async function pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { - const meta = await fetchMeta(); - - if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; - - // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 - push.setVapidDetails(config.url, - meta.swPublicKey, - meta.swPrivateKey); - - // Fetch - const subscriptions = await SwSubscriptions.findBy({ - userId: userId, - }); - - for (const subscription of subscriptions) { - const pushSubscription = { - endpoint: subscription.endpoint, - keys: { - auth: subscription.auth, - p256dh: subscription.publickey, - }, - }; - - push.sendNotification(pushSubscription, JSON.stringify({ - type, - body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body, - userId, - dateTime: (new Date()).getTime(), - }), { - proxy: config.proxy, - }).catch((err: any) => { - //swLogger.info(err.statusCode); - //swLogger.info(err.headers); - //swLogger.info(err.body); - - if (err.statusCode === 410) { - SwSubscriptions.delete({ - userId: userId, - endpoint: subscription.endpoint, - auth: subscription.auth, - publickey: subscription.publickey, - }); - } - }); - } -} diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts deleted file mode 100644 index df7d125d0b..0000000000 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Instance } from '@/models/entities/instance.js'; -import { Instances } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { Cache } from '@/misc/cache.js'; - -const cache = new Cache(1000 * 60 * 60); - -export async function registerOrFetchInstanceDoc(host: string): Promise { - host = toPuny(host); - - const cached = cache.get(host); - if (cached) return cached; - - const index = await Instances.findOneBy({ host }); - - if (index == null) { - const i = await Instances.insert({ - id: genId(), - host, - caughtAt: new Date(), - lastCommunicatedAt: new Date(), - }).then(x => Instances.findOneByOrFail(x.identifiers[0])); - - cache.set(host, i); - return i; - } else { - cache.set(host, index); - return index; - } -} diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts deleted file mode 100644 index 6bc4304436..0000000000 --- a/packages/backend/src/services/relay.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { IsNull } from 'typeorm'; -import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js'; -import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { deliver } from '@/queue/index.js'; -import { ILocalUser, User } from '@/models/entities/user.js'; -import { Users, Relays } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { Cache } from '@/misc/cache.js'; -import { Relay } from '@/models/entities/relay.js'; -import { createSystemUser } from './create-system-user.js'; - -const ACTOR_USERNAME = 'relay.actor' as const; - -const relaysCache = new Cache(1000 * 60 * 10); - -export async function getRelayActor(): Promise { - const user = await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }); - - if (user) return user as ILocalUser; - - const created = await createSystemUser(ACTOR_USERNAME); - return created as ILocalUser; -} - -export async function addRelay(inbox: string) { - const relay = await Relays.insert({ - id: genId(), - inbox, - status: 'requesting', - }).then(x => Relays.findOneByOrFail(x.identifiers[0])); - - const relayActor = await getRelayActor(); - const follow = await renderFollowRelay(relay, relayActor); - const activity = renderActivity(follow); - deliver(relayActor, activity, relay.inbox); - - return relay; -} - -export async function removeRelay(inbox: string) { - const relay = await Relays.findOneBy({ - inbox, - }); - - if (relay == null) { - throw 'relay not found'; - } - - const relayActor = await getRelayActor(); - const follow = renderFollowRelay(relay, relayActor); - const undo = renderUndo(follow, relayActor); - const activity = renderActivity(undo); - deliver(relayActor, activity, relay.inbox); - - await Relays.delete(relay.id); -} - -export async function listRelay() { - const relays = await Relays.find(); - return relays; -} - -export async function relayAccepted(id: string) { - const result = await Relays.update(id, { - status: 'accepted', - }); - - return JSON.stringify(result); -} - -export async function relayRejected(id: string) { - const result = await Relays.update(id, { - status: 'rejected', - }); - - return JSON.stringify(result); -} - -export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) { - if (activity == null) return; - - const relays = await relaysCache.fetch(null, () => Relays.findBy({ - status: 'accepted', - })); - if (relays.length === 0) return; - - // TODO - //const copy = structuredClone(activity); - const copy = JSON.parse(JSON.stringify(activity)); - if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; - - const signed = await attachLdSignature(copy, user); - - for (const relay of relays) { - deliver(user, signed, relay.inbox); - } -} diff --git a/packages/backend/src/services/send-email-notification.ts b/packages/backend/src/services/send-email-notification.ts deleted file mode 100644 index 4a2f94b425..0000000000 --- a/packages/backend/src/services/send-email-notification.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { UserProfiles } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { sendEmail } from './send-email.js'; -import { I18n } from '@/misc/i18n.js'; -import * as Acct from '@/misc/acct.js'; -// TODO -//const locales = await import('../../../../locales/index.js'); - -// TODO: locale ファイルをクライアント用とサーバー用で分けたい - -async function follow(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; - const locale = locales[userProfile.lang || 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ -} - -async function receiveFollowRequest(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; - const locale = locales[userProfile.lang || 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ -} - -export const sendEmailNotification = { - follow, - receiveFollowRequest, -}; diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts deleted file mode 100644 index b35d22548c..0000000000 --- a/packages/backend/src/services/send-email.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as nodemailer from 'nodemailer'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import Logger from './logger.js'; -import config from '@/config/index.js'; - -export const logger = new Logger('email'); - -export async function sendEmail(to: string, subject: string, html: string, text: string) { - const meta = await fetchMeta(true); - - const iconUrl = `${config.url}/static-assets/mi-white.png`; - const emailSettingUrl = `${config.url}/settings/email`; - - const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; - - const transporter = nodemailer.createTransport({ - host: meta.smtpHost, - port: meta.smtpPort, - secure: meta.smtpSecure, - ignoreTLS: !enableAuth, - proxy: config.proxySmtp, - auth: enableAuth ? { - user: meta.smtpUser, - pass: meta.smtpPass, - } : undefined, - } as any); - - try { - // TODO: htmlサニタイズ - const info = await transporter.sendMail({ - from: meta.email!, - to: to, - subject: subject, - text: text, - html: ` - - - - ${ subject } - - - -
-
- -
-
-

${ subject }

-
${ html }
-
- -
- - -`, - }); - - logger.info(`Message sent: ${info.messageId}`); - } catch (err) { - logger.error(err as Error); - throw err; - } -} diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts deleted file mode 100644 index 9fa2b97135..0000000000 --- a/packages/backend/src/services/stream.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { redisClient } from '../db/redis.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import config from '@/config/index.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { Channel } from '@/models/entities/channel.js'; -import { - StreamChannels, - AdminStreamTypes, - AntennaStreamTypes, - BroadcastTypes, - ChannelStreamTypes, - DriveStreamTypes, - GroupMessagingStreamTypes, - InternalStreamTypes, - MainStreamTypes, - MessagingIndexStreamTypes, - MessagingStreamTypes, - NoteStreamTypes, - UserListStreamTypes, - UserStreamTypes, -} from '@/server/api/stream/types.js'; -import { Packed } from '@/misc/schema.js'; - -class Publisher { - private publish = (channel: StreamChannels, type: string | null, value?: any): void => { - const message = type == null ? value : value == null ? - { type: type, body: null } : - { type: type, body: value }; - - redisClient.publish(config.host, JSON.stringify({ - channel: channel, - message: message, - })); - }; - - public publishInternalEvent = (type: K, value?: InternalStreamTypes[K]): void => { - this.publish('internal', type, typeof value === 'undefined' ? null : value); - }; - - public publishUserEvent = (userId: User['id'], type: K, value?: UserStreamTypes[K]): void => { - this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishBroadcastStream = (type: K, value?: BroadcastTypes[K]): void => { - this.publish('broadcast', type, typeof value === 'undefined' ? null : value); - }; - - public publishMainStream = (userId: User['id'], type: K, value?: MainStreamTypes[K]): void => { - this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishDriveStream = (userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => { - this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishNoteStream = (noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => { - this.publish(`noteStream:${noteId}`, type, { - id: noteId, - body: value, - }); - }; - - public publishChannelStream = (channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => { - this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishUserListStream = (listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => { - this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishAntennaStream = (antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => { - this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => { - this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishGroupMessagingStream = (groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => { - this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishMessagingIndexStream = (userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => { - this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishNotesStream = (note: Packed<'Note'>): void => { - this.publish('notesStream', null, note); - }; - - public publishAdminStream = (userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => { - this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; -} - -const publisher = new Publisher(); - -export default publisher; - -export const publishInternalEvent = publisher.publishInternalEvent; -export const publishUserEvent = publisher.publishUserEvent; -export const publishBroadcastStream = publisher.publishBroadcastStream; -export const publishMainStream = publisher.publishMainStream; -export const publishDriveStream = publisher.publishDriveStream; -export const publishNoteStream = publisher.publishNoteStream; -export const publishNotesStream = publisher.publishNotesStream; -export const publishChannelStream = publisher.publishChannelStream; -export const publishUserListStream = publisher.publishUserListStream; -export const publishAntennaStream = publisher.publishAntennaStream; -export const publishMessagingStream = publisher.publishMessagingStream; -export const publishGroupMessagingStream = publisher.publishGroupMessagingStream; -export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; -export const publishAdminStream = publisher.publishAdminStream; diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts deleted file mode 100644 index e96b06a351..0000000000 --- a/packages/backend/src/services/suspend-user.ts +++ /dev/null @@ -1,37 +0,0 @@ -import renderDelete from '@/remote/activitypub/renderer/delete.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { deliver } from '@/queue/index.js'; -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Users, Followings } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { publishInternalEvent } from '@/services/stream.js'; - -export async function doPostSuspend(user: { id: User['id']; host: User['host'] }) { - publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - - if (Users.isLocalUser(user)) { - // 知り得る全SharedInboxにDelete配信 - const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user)); - - const queue: string[] = []; - - const followings = await Followings.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - deliver(user, content, inbox); - } - } -} diff --git a/packages/backend/src/services/unsuspend-user.ts b/packages/backend/src/services/unsuspend-user.ts deleted file mode 100644 index 44a0d01ca2..0000000000 --- a/packages/backend/src/services/unsuspend-user.ts +++ /dev/null @@ -1,38 +0,0 @@ -import renderDelete from '@/remote/activitypub/renderer/delete.js'; -import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { deliver } from '@/queue/index.js'; -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Users, Followings } from '@/models/index.js'; -import { Not, IsNull } from 'typeorm'; -import { publishInternalEvent } from '@/services/stream.js'; - -export async function doPostUnsuspend(user: User) { - publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); - - if (Users.isLocalUser(user)) { - // 知り得る全SharedInboxにUndo Delete配信 - const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user)); - - const queue: string[] = []; - - const followings = await Followings.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - deliver(user as any, content, inbox); - } - } -} diff --git a/packages/backend/src/services/update-hashtag.ts b/packages/backend/src/services/update-hashtag.ts deleted file mode 100644 index 23b210b7a9..0000000000 --- a/packages/backend/src/services/update-hashtag.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Hashtags, Users } from '@/models/index.js'; -import { hashtagChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { Hashtag } from '@/models/entities/hashtag.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; - -export async function updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) { - for (const tag of tags) { - await updateHashtag(user, tag); - } -} - -export async function updateUsertags(user: User, tags: string[]) { - for (const tag of tags) { - await updateHashtag(user, tag, true, true); - } - - for (const tag of (user.tags || []).filter(x => !tags.includes(x))) { - await updateHashtag(user, tag, true, false); - } -} - -export async function updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) { - tag = normalizeForSearch(tag); - - const index = await Hashtags.findOneBy({ name: tag }); - - if (index == null && !inc) return; - - if (index != null) { - const q = Hashtags.createQueryBuilder('tag').update() - .where('name = :name', { name: tag }); - - const set = {} as any; - - if (isUserAttached) { - if (inc) { - // 自分が初めてこのタグを使ったなら - if (!index.attachedUserIds.some(id => id === user.id)) { - set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`; - set.attachedUsersCount = () => `"attachedUsersCount" + 1`; - } - // 自分が(ローカル内で)初めてこのタグを使ったなら - if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) { - set.attachedLocalUserIds = () => `array_append("attachedLocalUserIds", '${user.id}')`; - set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" + 1`; - } - // 自分が(リモートで)初めてこのタグを使ったなら - if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) { - set.attachedRemoteUserIds = () => `array_append("attachedRemoteUserIds", '${user.id}')`; - set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" + 1`; - } - } else { - set.attachedUserIds = () => `array_remove("attachedUserIds", '${user.id}')`; - set.attachedUsersCount = () => `"attachedUsersCount" - 1`; - if (Users.isLocalUser(user)) { - set.attachedLocalUserIds = () => `array_remove("attachedLocalUserIds", '${user.id}')`; - set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" - 1`; - } else { - set.attachedRemoteUserIds = () => `array_remove("attachedRemoteUserIds", '${user.id}')`; - set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" - 1`; - } - } - } else { - // 自分が初めてこのタグを使ったなら - if (!index.mentionedUserIds.some(id => id === user.id)) { - set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`; - set.mentionedUsersCount = () => `"mentionedUsersCount" + 1`; - } - // 自分が(ローカル内で)初めてこのタグを使ったなら - if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) { - set.mentionedLocalUserIds = () => `array_append("mentionedLocalUserIds", '${user.id}')`; - set.mentionedLocalUsersCount = () => `"mentionedLocalUsersCount" + 1`; - } - // 自分が(リモートで)初めてこのタグを使ったなら - if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) { - set.mentionedRemoteUserIds = () => `array_append("mentionedRemoteUserIds", '${user.id}')`; - set.mentionedRemoteUsersCount = () => `"mentionedRemoteUsersCount" + 1`; - } - } - - if (Object.keys(set).length > 0) { - q.set(set); - q.execute(); - } - } else { - if (isUserAttached) { - Hashtags.insert({ - id: genId(), - name: tag, - mentionedUserIds: [], - mentionedUsersCount: 0, - mentionedLocalUserIds: [], - mentionedLocalUsersCount: 0, - mentionedRemoteUserIds: [], - mentionedRemoteUsersCount: 0, - attachedUserIds: [user.id], - attachedUsersCount: 1, - attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], - attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], - attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, - } as Hashtag); - } else { - Hashtags.insert({ - id: genId(), - name: tag, - mentionedUserIds: [user.id], - mentionedUsersCount: 1, - mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], - mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], - mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, - attachedUserIds: [], - attachedUsersCount: 0, - attachedLocalUserIds: [], - attachedLocalUsersCount: 0, - attachedRemoteUserIds: [], - attachedRemoteUsersCount: 0, - } as Hashtag); - } - } - - if (!isUserAttached) { - hashtagChart.update(tag, user); - } -} diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts deleted file mode 100644 index 407301f2fd..0000000000 --- a/packages/backend/src/services/user-cache.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; -import { subsdcriber } from '@/db/redis.js'; - -export const userByIdCache = new Cache(Infinity); -export const localUserByNativeTokenCache = new Cache(Infinity); -export const localUserByIdCache = new Cache(Infinity); -export const uriPersonCache = new Cache(Infinity); - -subsdcriber.on('message', async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message; - switch (type) { - case 'userChangeSuspendedState': - case 'userChangeSilencedState': - case 'userChangeModeratorState': - case 'remoteUserUpdated': { - const user = await Users.findOneByOrFail({ id: body.id }); - userByIdCache.set(user.id, user); - for (const [k, v] of uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { - uriPersonCache.set(k, user); - } - } - if (Users.isLocalUser(user)) { - localUserByNativeTokenCache.set(user.token, user); - localUserByIdCache.set(user.id, user); - } - break; - } - case 'userTokenRegenerated': { - const user = await Users.findOneByOrFail({ id: body.id }) as ILocalUser; - localUserByNativeTokenCache.delete(body.oldToken); - localUserByNativeTokenCache.set(body.newToken, user); - break; - } - default: - break; - } - } -}); diff --git a/packages/backend/src/services/user-list/push.ts b/packages/backend/src/services/user-list/push.ts deleted file mode 100644 index d073afcd3a..0000000000 --- a/packages/backend/src/services/user-list/push.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { publishUserListStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserListJoinings, Users } from '@/models/index.js'; -import { UserListJoining } from '@/models/entities/user-list-joining.js'; -import { genId } from '@/misc/gen-id.js'; -import { fetchProxyAccount } from '@/misc/fetch-proxy-account.js'; -import createFollowing from '../following/create.js'; - -export async function pushUserToUserList(target: User, list: UserList) { - await UserListJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: target.id, - userListId: list.id, - } as UserListJoining); - - publishUserListStream(list.id, 'userAdded', await Users.pack(target)); - - // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする - if (Users.isRemoteUser(target)) { - const proxy = await fetchProxyAccount(); - if (proxy) { - createFollowing(proxy, target); - } - } -} diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts deleted file mode 100644 index b5fa99b935..0000000000 --- a/packages/backend/src/services/validate-email-for-account.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { validate as validateEmail } from 'deep-email-validator'; -import { UserProfiles } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; - -export async function validateEmailForAccount(emailAddress: string): Promise<{ - available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; -}> { - const meta = await fetchMeta(); - - const exist = await UserProfiles.countBy({ - emailVerified: true, - email: emailAddress, - }); - - const validated = meta.enableActiveEmailValidation ? await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }) : { valid: true }; - - const available = exist === 0 && validated.valid; - - return { - available, - reason: available ? null : - exist !== 0 ? 'used' : - validated.reason === 'regex' ? 'format' : - validated.reason === 'disposable' ? 'disposable' : - validated.reason === 'mx' ? 'mx' : - validated.reason === 'smtp' ? 'smtp' : - null, - }; -} diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs index d83dc37d2f..41ecea0c3f 100644 --- a/packages/backend/test/.eslintrc.cjs +++ b/packages/backend/test/.eslintrc.cjs @@ -6,6 +6,6 @@ module.exports = { extends: ['../.eslintrc.cjs'], env: { node: true, - mocha: true, + jest: true, }, }; diff --git a/packages/backend/test/_e2e/api-visibility.ts b/packages/backend/test/_e2e/api-visibility.ts new file mode 100644 index 0000000000..9c21840844 --- /dev/null +++ b/packages/backend/test/_e2e/api-visibility.ts @@ -0,0 +1,477 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, startServer, shutdownServer } from '../utils.js'; + +describe('API visibility', () => { + let p: childProcess.ChildProcess; + + beforeAll(async () => { + p = await startServer(); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + describe('Note visibility', () => { + //#region vars + /** ヒロイン */ + let alice: any; + /** フォロワー */ + let follower: any; + /** 非フォロワー */ + let other: any; + /** 非フォロワーでもリプライやメンションをされた人 */ + let target: any; + /** specified mentionでmentionを飛ばされる人 */ + let target2: any; + + /** public-post */ + let pub: any; + /** home-post */ + let home: any; + /** followers-post */ + let fol: any; + /** specified-post */ + let spe: any; + + /** public-reply to target's post */ + let pubR: any; + /** home-reply to target's post */ + let homeR: any; + /** followers-reply to target's post */ + let folR: any; + /** specified-reply to target's post */ + let speR: any; + + /** public-mention to target */ + let pubM: any; + /** home-mention to target */ + let homeM: any; + /** followers-mention to target */ + let folM: any; + /** specified-mention to target */ + let speM: any; + + /** reply target post */ + let tgt: any; + //#endregion + + const show = async (noteId: any, by: any) => { + return await request('/notes/show', { + noteId, + }, by); + }; + + beforeAll(async () => { + //#region prepare + // signup + alice = await signup({ username: 'alice' }); + follower = await signup({ username: 'follower' }); + other = await signup({ username: 'other' }); + target = await signup({ username: 'target' }); + target2 = await signup({ username: 'target2' }); + + // follow alice <= follower + await request('/following/create', { userId: alice.id }, follower); + + // normal posts + pub = await post(alice, { text: 'x', visibility: 'public' }); + home = await post(alice, { text: 'x', visibility: 'home' }); + fol = await post(alice, { text: 'x', visibility: 'followers' }); + spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); + + // replies + tgt = await post(target, { text: 'y', visibility: 'public' }); + pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); + homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); + folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); + speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); + + // mentions + pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); + homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); + folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); + speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); + //#endregion + }); + + //#region show post + // public + it('[show] public-postを自分が見れる', async () => { + const res = await show(pub.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-postをフォロワーが見れる', async () => { + const res = await show(pub.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-postを非フォロワーが見れる', async () => { + const res = await show(pub.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-postを未認証が見れる', async () => { + const res = await show(pub.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // home + it('[show] home-postを自分が見れる', async () => { + const res = await show(home.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-postをフォロワーが見れる', async () => { + const res = await show(home.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-postを非フォロワーが見れる', async () => { + const res = await show(home.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-postを未認証が見れる', async () => { + const res = await show(home.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // followers + it('[show] followers-postを自分が見れる', async () => { + const res = await show(fol.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] followers-postをフォロワーが見れる', async () => { + const res = await show(fol.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] followers-postを非フォロワーが見れない', async () => { + const res = await show(fol.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] followers-postを未認証が見れない', async () => { + const res = await show(fol.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + it('[show] specified-postを自分が見れる', async () => { + const res = await show(spe.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] specified-postを指定ユーザーが見れる', async () => { + const res = await show(spe.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] specified-postをフォロワーが見れない', async () => { + const res = await show(spe.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-postを非フォロワーが見れない', async () => { + const res = await show(spe.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-postを未認証が見れない', async () => { + const res = await show(spe.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region show reply + // public + it('[show] public-replyを自分が見れる', async () => { + const res = await show(pubR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-replyをされた人が見れる', async () => { + const res = await show(pubR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-replyをフォロワーが見れる', async () => { + const res = await show(pubR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-replyを非フォロワーが見れる', async () => { + const res = await show(pubR.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] public-replyを未認証が見れる', async () => { + const res = await show(pubR.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // home + it('[show] home-replyを自分が見れる', async () => { + const res = await show(homeR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-replyをされた人が見れる', async () => { + const res = await show(homeR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-replyをフォロワーが見れる', async () => { + const res = await show(homeR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-replyを非フォロワーが見れる', async () => { + const res = await show(homeR.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] home-replyを未認証が見れる', async () => { + const res = await show(homeR.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // followers + it('[show] followers-replyを自分が見れる', async () => { + const res = await show(folR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => { + const res = await show(folR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] followers-replyをフォロワーが見れる', async () => { + const res = await show(folR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] followers-replyを非フォロワーが見れない', async () => { + const res = await show(folR.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] followers-replyを未認証が見れない', async () => { + const res = await show(folR.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + it('[show] specified-replyを自分が見れる', async () => { + const res = await show(speR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] specified-replyを指定ユーザーが見れる', async () => { + const res = await show(speR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] specified-replyをされた人が指定されてなくても見れる', async () => { + const res = await show(speR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + it('[show] specified-replyをフォロワーが見れない', async () => { + const res = await show(speR.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-replyを非フォロワーが見れない', async () => { + const res = await show(speR.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-replyを未認証が見れない', async () => { + const res = await show(speR.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region show mention + // public + it('[show] public-mentionを自分が見れる', async () => { + const res = await show(pubM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] public-mentionをされた人が見れる', async () => { + const res = await show(pubM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] public-mentionをフォロワーが見れる', async () => { + const res = await show(pubM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] public-mentionを非フォロワーが見れる', async () => { + const res = await show(pubM.id, other); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] public-mentionを未認証が見れる', async () => { + const res = await show(pubM.id, null); + assert.strictEqual(res.body.text, '@target x'); + }); + + // home + it('[show] home-mentionを自分が見れる', async () => { + const res = await show(homeM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] home-mentionをされた人が見れる', async () => { + const res = await show(homeM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] home-mentionをフォロワーが見れる', async () => { + const res = await show(homeM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] home-mentionを非フォロワーが見れる', async () => { + const res = await show(homeM.id, other); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] home-mentionを未認証が見れる', async () => { + const res = await show(homeM.id, null); + assert.strictEqual(res.body.text, '@target x'); + }); + + // followers + it('[show] followers-mentionを自分が見れる', async () => { + const res = await show(folM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => { + const res = await show(folM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] followers-mentionをフォロワーが見れる', async () => { + const res = await show(folM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + it('[show] followers-mentionを非フォロワーが見れない', async () => { + const res = await show(folM.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] followers-mentionを未認証が見れない', async () => { + const res = await show(folM.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + it('[show] specified-mentionを自分が見れる', async () => { + const res = await show(speM.id, alice); + assert.strictEqual(res.body.text, '@target2 x'); + }); + + it('[show] specified-mentionを指定ユーザーが見れる', async () => { + const res = await show(speM.id, target); + assert.strictEqual(res.body.text, '@target2 x'); + }); + + it('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => { + const res = await show(speM.id, target2); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-mentionをフォロワーが見れない', async () => { + const res = await show(speM.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-mentionを非フォロワーが見れない', async () => { + const res = await show(speM.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + it('[show] specified-mentionを未認証が見れない', async () => { + const res = await show(speM.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region HTL + it('[HTL] public-post が 自分が見れる', async () => { + const res = await request('/notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === pub.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + it('[HTL] public-post が 非フォロワーから見れない', async () => { + const res = await request('/notes/timeline', { limit: 100 }, other); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === pub.id); + assert.strictEqual(notes.length, 0); + }); + + it('[HTL] followers-post が フォロワーから見れる', async () => { + const res = await request('/notes/timeline', { limit: 100 }, follower); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === fol.id); + assert.strictEqual(notes[0].text, 'x'); + }); + //#endregion + + //#region RTL + it('[replies] followers-reply が フォロワーから見れる', async () => { + const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { + const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes.length, 0); + }); + + it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { + const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + //#endregion + + //#region MTL + it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { + const res = await request('/notes/mentions', { limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { + const res = await request('/notes/mentions', { limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folM.id); + assert.strictEqual(notes[0].text, '@target x'); + }); + //#endregion + }); +}); +*/ diff --git a/packages/backend/test/_e2e/api.ts b/packages/backend/test/_e2e/api.ts new file mode 100644 index 0000000000..3c08022031 --- /dev/null +++ b/packages/backend/test/_e2e/api.ts @@ -0,0 +1,83 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from '../utils.js'; + +describe('API', () => { + let p: childProcess.ChildProcess; + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + describe('General validation', () => { + it('wrong type', async(async () => { + const res = await request('/test', { + required: true, + string: 42, + }); + assert.strictEqual(res.status, 400); + })); + + it('missing require param', async(async () => { + const res = await request('/test', { + string: 'a', + }); + assert.strictEqual(res.status, 400); + })); + + it('invalid misskey:id (empty string)', async(async () => { + const res = await request('/test', { + required: true, + id: '', + }); + assert.strictEqual(res.status, 400); + })); + + it('valid misskey:id', async(async () => { + const res = await request('/test', { + required: true, + id: '8wvhjghbxu', + }); + assert.strictEqual(res.status, 200); + })); + + it('default value', async(async () => { + const res = await request('/test', { + required: true, + string: 'a', + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.default, 'hello'); + })); + + it('can set null even if it has default value', async(async () => { + const res = await request('/test', { + required: true, + nullableDefault: null, + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.nullableDefault, null); + })); + + it('cannot set undefined if it has default value', async(async () => { + const res = await request('/test', { + required: true, + nullableDefault: undefined, + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.nullableDefault, 'hello'); + })); + }); +}); diff --git a/packages/backend/test/_e2e/block.ts b/packages/backend/test/_e2e/block.ts new file mode 100644 index 0000000000..bb31983a32 --- /dev/null +++ b/packages/backend/test/_e2e/block.ts @@ -0,0 +1,85 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, startServer, shutdownServer } from '../utils.js'; + +describe('Block', () => { + let p: childProcess.ChildProcess; + + // alice blocks bob + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + it('Block作成', async () => { + const res = await request('/blocking/create', { + userId: bob.id, + }, alice); + + assert.strictEqual(res.status, 200); + }); + + it('ブロックされているユーザーをフォローできない', async () => { + const res = await request('/following/create', { userId: alice.id }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + }); + + it('ブロックされているユーザーにリアクションできない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + }); + + it('ブロックされているユーザーに返信できない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + }); + + it('ブロックされているユーザーのノートをRenoteできない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + }); + + // TODO: ユーザーリストに入れられないテスト + + // TODO: ユーザーリストから除外されるテスト + + it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await request('/notes/local-timeline', {}, bob); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); +}); diff --git a/packages/backend/test/_e2e/endpoints.ts b/packages/backend/test/_e2e/endpoints.ts new file mode 100644 index 0000000000..05b74a65d4 --- /dev/null +++ b/packages/backend/test/_e2e/endpoints.ts @@ -0,0 +1,881 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import * as openapi from '@redocly/openapi-core'; +import { startServer, signup, post, request, simpleGet, port, shutdownServer, api } from '../utils.js'; + +describe('Endpoints', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let bob: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + describe('signup', () => { + it('不正なユーザー名でアカウントが作成できない', async () => { + const res = await request('api/signup', { + username: 'test.', + password: 'test', + }); + assert.strictEqual(res.status, 400); + }); + + it('空のパスワードでアカウントが作成できない', async () => { + const res = await request('api/signup', { + username: 'test', + password: '', + }); + assert.strictEqual(res.status, 400); + }); + + it('正しくアカウントが作成できる', async () => { + const me = { + username: 'test1', + password: 'test1', + }; + + const res = await request('api/signup', me); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.username, me.username); + }); + + it('同じユーザー名のアカウントは作成できない', async () => { + const res = await request('api/signup', { + username: 'test1', + password: 'test1', + }); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('signin', () => { + it('間違ったパスワードでサインインできない', async () => { + const res = await request('api/signin', { + username: 'test1', + password: 'bar', + }); + + assert.strictEqual(res.status, 403); + }); + + it('クエリをインジェクションできない', async () => { + const res = await request('api/signin', { + username: 'test1', + password: { + $gt: '', + }, + }); + + assert.strictEqual(res.status, 400); + }); + + it('正しい情報でサインインできる', async () => { + const res = await request('api/signin', { + username: 'test1', + password: 'test1', + }); + + assert.strictEqual(res.status, 200); + }); + }); + + describe('i/update', () => { + it('アカウント設定を更新できる', async () => { + const myName = '大室櫻子'; + const myLocation = '七森中'; + const myBirthday = '2000-09-07'; + + const res = await api('/i/update', { + name: myName, + location: myLocation, + birthday: myBirthday, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, myName); + assert.strictEqual(res.body.location, myLocation); + assert.strictEqual(res.body.birthday, myBirthday); + }); + + it('名前を空白にできない', async () => { + const res = await api('/i/update', { + name: ' ', + }, alice); + assert.strictEqual(res.status, 400); + }); + + it('誕生日の設定を削除できる', async () => { + await api('/i/update', { + birthday: '2000-09-07', + }, alice); + + const res = await api('/i/update', { + birthday: null, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.birthday, null); + }); + + it('不正な誕生日の形式で怒られる', async () => { + const res = await api('/i/update', { + birthday: '2000/09/07', + }, alice); + assert.strictEqual(res.status, 400); + }); + }); + + describe('users/show', () => { + it('ユーザーが取得できる', async () => { + const res = await api('/users/show', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, alice.id); + }); + + it('ユーザーが存在しなかったら怒る', async () => { + const res = await api('/users/show', { + userId: '000000000000000000000000', + }); + assert.strictEqual(res.status, 400); + }); + + it('間違ったIDで怒られる', async () => { + const res = await api('/users/show', { + userId: 'kyoppie', + }); + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/show', () => { + it('投稿が取得できる', async () => { + const myPost = await post(alice, { + text: 'test', + }); + + const res = await api('/notes/show', { + noteId: myPost.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, myPost.id); + assert.strictEqual(res.body.text, myPost.text); + }); + + it('投稿が存在しなかったら怒る', async () => { + const res = await api('/notes/show', { + noteId: '000000000000000000000000', + }); + assert.strictEqual(res.status, 400); + }); + + it('間違ったIDで怒られる', async () => { + const res = await api('/notes/show', { + noteId: 'kyoppie', + }); + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/reactions/create', () => { + it('リアクションできる', async () => { + const bobPost = await post(bob); + + const alice = await signup({ username: 'alice' }); + const res = await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + + const resNote = await api('/notes/show', { + noteId: bobPost.id, + }, alice); + + assert.strictEqual(resNote.status, 200); + assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); + }); + + it('自分の投稿にもリアクションできる', async () => { + const myPost = await post(alice); + + const res = await api('/notes/reactions/create', { + noteId: myPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + }); + + it('二重にリアクションできない', async () => { + const bobPost = await post(bob); + + await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🥰', + }, alice); + + const res = await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('存在しない投稿にはリアクションできない', async () => { + const res = await api('/notes/reactions/create', { + noteId: '000000000000000000000000', + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('空のパラメータで怒られる', async () => { + const res = await api('/notes/reactions/create', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + it('間違ったIDで怒られる', async () => { + const res = await api('/notes/reactions/create', { + noteId: 'kyoppie', + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('following/create', () => { + it('フォローできる', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 200); + }); + + it('既にフォローしている場合は怒る', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 400); + }); + + it('存在しないユーザーはフォローできない', async () => { + const res = await api('/following/create', { + userId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('自分自身はフォローできない', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('空のパラメータで怒られる', async () => { + const res = await api('/following/create', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + it('間違ったIDで怒られる', async () => { + const res = await api('/following/create', { + userId: 'foo', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('following/delete', () => { + it('フォロー解除できる', async () => { + await api('/following/create', { + userId: alice.id, + }, bob); + + const res = await api('/following/delete', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 200); + }); + + it('フォローしていない場合は怒る', async () => { + const res = await api('/following/delete', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 400); + }); + + it('存在しないユーザーはフォロー解除できない', async () => { + const res = await api('/following/delete', { + userId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('自分自身はフォロー解除できない', async () => { + const res = await api('/following/delete', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('空のパラメータで怒られる', async () => { + const res = await api('/following/delete', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + it('間違ったIDで怒られる', async () => { + const res = await api('/following/delete', { + userId: 'kyoppie', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + /* + describe('/i', () => { + it('', async () => { + }); + }); + */ +}); + +/* +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; + +describe('API: Endpoints', () => { + let p: childProcess.ChildProcess; + let alice: any; + let bob: any; + let carol: any; + + before(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }); + + after(async () => { + await shutdownServer(p); + }); + + describe('drive', () => { + it('ドライブ情報を取得できる', async () => { + await uploadFile({ + userId: alice.id, + size: 256 + }); + await uploadFile({ + userId: alice.id, + size: 512 + }); + await uploadFile({ + userId: alice.id, + size: 1024 + }); + const res = await api('/drive', {}, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + expect(res.body).have.property('usage').eql(1792); + })); + }); + + describe('drive/files/create', () => { + it('ファイルを作成できる', async () => { + const res = await uploadFile(alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Lenna.png'); + })); + + it('ファイルに名前を付けられる', async () => { + const res = await assert.request(server) + .post('/drive/files/create') + .field('i', alice.token) + .field('name', 'Belmond.png') + .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); + + expect(res).have.status(200); + expect(res.body).be.a('object'); + expect(res.body).have.property('name').eql('Belmond.png'); + })); + + it('ファイル無しで怒られる', async () => { + const res = await api('/drive/files/create', {}, alice); + + assert.strictEqual(res.status, 400); + })); + + it('SVGファイルを作成できる', async () => { + const res = await uploadFile(alice, __dirname + '/resources/image.svg'); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); + })); + }); + + describe('drive/files/update', () => { + it('名前を更新できる', async () => { + const file = await uploadFile(alice); + const newName = 'いちごパスタ.png'; + + const res = await api('/drive/files/update', { + fileId: file.id, + name: newName + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, newName); + })); + + it('他人のファイルは更新できない', async () => { + const file = await uploadFile(bob); + + const res = await api('/drive/files/update', { + fileId: file.id, + name: 'いちごパスタ.png' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('親フォルダを更新できる', async () => { + const file = await uploadFile(alice); + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, folder.id); + })); + + it('親フォルダを無しにできる', async () => { + const file = await uploadFile(alice); + + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id + }, alice); + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: null + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, null); + })); + + it('他人のフォルダには入れられない', async () => { + const file = await uploadFile(alice); + const folder = (await api('/drive/folders/create', { + name: 'test' + }, bob)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('存在しないフォルダで怒られる', async () => { + const file = await uploadFile(alice); + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: '000000000000000000000000' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('不正なフォルダIDで怒られる', async () => { + const file = await uploadFile(alice); + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: 'foo' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('ファイルが存在しなかったら怒る', async () => { + const res = await api('/drive/files/update', { + fileId: '000000000000000000000000', + name: 'いちごパスタ.png' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('間違ったIDで怒られる', async () => { + const res = await api('/drive/files/update', { + fileId: 'kyoppie', + name: 'いちごパスタ.png' + }, alice); + + assert.strictEqual(res.status, 400); + })); + }); + + describe('drive/folders/create', () => { + it('フォルダを作成できる', async () => { + const res = await api('/drive/folders/create', { + name: 'test' + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'test'); + })); + }); + + describe('drive/folders/update', () => { + it('名前を更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + name: 'new name' + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'new name'); + })); + + it('他人のフォルダを更新できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, bob)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + name: 'new name' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('親フォルダを更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent' + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, parentFolder.id); + })); + + it('親フォルダを無しに更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent' + }, alice)).body; + await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: null + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, null); + })); + + it('他人のフォルダを親フォルダに設定できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent' + }, bob)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('フォルダが循環するような構造にできない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent' + }, alice)).body; + await api('/drive/folders/update', { + folderId: parentFolder.id, + parentId: folder.id + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('フォルダが循環するような構造にできない(再帰的)', async () => { + const folderA = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const folderB = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + const folderC = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + await api('/drive/folders/update', { + folderId: folderB.id, + parentId: folderA.id + }, alice); + await api('/drive/folders/update', { + folderId: folderC.id, + parentId: folderB.id + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folderA.id, + parentId: folderC.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('フォルダが循環するような構造にできない(自身)', async () => { + const folderA = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folderA.id, + parentId: folderA.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('存在しない親フォルダを設定できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: '000000000000000000000000' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('不正な親フォルダIDで怒られる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test' + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: 'foo' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('存在しないフォルダを更新できない', async () => { + const res = await api('/drive/folders/update', { + folderId: '000000000000000000000000' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('不正なフォルダIDで怒られる', async () => { + const res = await api('/drive/folders/update', { + folderId: 'foo' + }, alice); + + assert.strictEqual(res.status, 400); + })); + }); + + describe('messaging/messages/create', () => { + it('メッセージを送信できる', async () => { + const res = await api('/messaging/messages/create', { + userId: bob.id, + text: 'test' + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.text, 'test'); + })); + + it('自分自身にはメッセージを送信できない', async () => { + const res = await api('/messaging/messages/create', { + userId: alice.id, + text: 'Yo' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('存在しないユーザーにはメッセージを送信できない', async () => { + const res = await api('/messaging/messages/create', { + userId: '000000000000000000000000', + text: 'test' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('不正なユーザーIDで怒られる', async () => { + const res = await api('/messaging/messages/create', { + userId: 'foo', + text: 'test' + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('テキストが無くて怒られる', async () => { + const res = await api('/messaging/messages/create', { + userId: bob.id + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('文字数オーバーで怒られる', async () => { + const res = await api('/messaging/messages/create', { + userId: bob.id, + text: '!'.repeat(1001) + }, alice); + + assert.strictEqual(res.status, 400); + })); + }); + + describe('notes/replies', () => { + it('自分に閲覧権限のない投稿は含まれない', async () => { + const alicePost = await post(alice, { + text: 'foo' + }); + + await post(bob, { + replyId: alicePost.id, + text: 'bar', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + + const res = await api('/notes/replies', { + noteId: alicePost.id + }, carol); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 0); + })); + }); + + describe('notes/timeline', () => { + it('フォロワー限定投稿が含まれる', async () => { + await api('/following/create', { + userId: alice.id + }, bob); + + const alicePost = await post(alice, { + text: 'foo', + visibility: 'followers' + }); + + const res = await api('/notes/timeline', {}, bob); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 1); + assert.strictEqual(res.body[0].id, alicePost.id); + })); + }); +}); +*/ diff --git a/packages/backend/test/_e2e/fetch-resource.ts b/packages/backend/test/_e2e/fetch-resource.ts new file mode 100644 index 0000000000..344022dec7 --- /dev/null +++ b/packages/backend/test/_e2e/fetch-resource.ts @@ -0,0 +1,205 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import * as openapi from '@redocly/openapi-core'; +import { startServer, signup, post, request, simpleGet, port, shutdownServer } from '../utils.js'; + +// Request Accept +const ONLY_AP = 'application/activity+json'; +const PREFER_AP = 'application/activity+json, */*'; +const PREFER_HTML = 'text/html, */*'; +const UNSPECIFIED = '*/*'; + +// Response Contet-Type +const AP = 'application/activity+json; charset=utf-8'; +const JSON = 'application/json; charset=utf-8'; +const HTML = 'text/html; charset=utf-8'; + +describe('Fetch resource', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let alicesPost: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + alicesPost = await post(alice, { + text: 'test', + }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + describe('Common', () => { + it('meta', async () => { + const res = await request('/meta', { + }); + + assert.strictEqual(res.status, 200); + }); + + it('GET root', async () => { + const res = await simpleGet('/'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + it('GET docs', async () => { + const res = await simpleGet('/docs/ja-JP/about'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + it('GET api-doc', async () => { + const res = await simpleGet('/api-doc'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + it('GET api.json', async () => { + const res = await simpleGet('/api.json'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, JSON); + }); + + it('Validate api.json', async () => { + const config = await openapi.loadConfig(); + const result = await openapi.bundle({ + config, + ref: `http://localhost:${port}/api.json`, + }); + + for (const problem of result.problems) { + console.log(`${problem.message} - ${problem.location[0]?.pointer}`); + } + + assert.strictEqual(result.problems.length, 0); + }); + + it('GET favicon.ico', async () => { + const res = await simpleGet('/favicon.ico'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/x-icon'); + }); + + it('GET apple-touch-icon.png', async () => { + const res = await simpleGet('/apple-touch-icon.png'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/png'); + }); + + it('GET twemoji svg', async () => { + const res = await simpleGet('/twemoji/2764.svg'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/svg+xml'); + }); + + it('GET twemoji svg with hyphen', async () => { + const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/svg+xml'); + }); + }); + + describe('/@:username', () => { + it('Only AP => AP', async () => { + const res = await simpleGet(`/@${alice.username}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer AP => AP', async () => { + const res = await simpleGet(`/@${alice.username}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer HTML => HTML', async () => { + const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + it('Unspecified => HTML', async () => { + const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + }); + + describe('/users/:id', () => { + it('Only AP => AP', async () => { + const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer AP => AP', async () => { + const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer HTML => Redirect to /@:username', async () => { + const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); + assert.strictEqual(res.status, 302); + assert.strictEqual(res.location, `/@${alice.username}`); + }); + + it('Undecided => HTML', async () => { + const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); + assert.strictEqual(res.status, 302); + assert.strictEqual(res.location, `/@${alice.username}`); + }); + }); + + describe('/notes/:id', () => { + it('Only AP => AP', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer AP => AP', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + it('Prefer HTML => HTML', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + it('Unspecified => HTML', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + }); + + describe('Feeds', () => { + it('RSS', async () => { + const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); + }); + + it('ATOM', async () => { + const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); + }); + + it('JSON', async () => { + const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/json; charset=utf-8'); + }); + }); +}); diff --git a/packages/backend/test/_e2e/ff-visibility.ts b/packages/backend/test/_e2e/ff-visibility.ts new file mode 100644 index 0000000000..38be0eba24 --- /dev/null +++ b/packages/backend/test/_e2e/ff-visibility.ts @@ -0,0 +1,167 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from '../utils.js'; + +describe('FF visibility', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + await request('/i/update', { + ffVisibility: 'public', + }, alice); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await request('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, alice); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, alice); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + await request('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 400); + assert.strictEqual(followersRes.status, 400); + }); + + it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + await request('/i/update', { + ffVisibility: 'followers', + }, alice); + + await request('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await request('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, alice); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, alice); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + await request('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await request('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 400); + assert.strictEqual(followersRes.status, 400); + }); + + describe('AP', () => { + it('ffVisibility が public 以外ならばAPからは取得できない', async () => { + { + await request('/i/update', { + ffVisibility: 'public', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(followersRes.status, 200); + } + { + await request('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + } + { + await request('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + } + }); + }); +}); diff --git a/packages/backend/test/_e2e/mute.ts b/packages/backend/test/_e2e/mute.ts new file mode 100644 index 0000000000..2313773678 --- /dev/null +++ b/packages/backend/test/_e2e/mute.ts @@ -0,0 +1,123 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, react, startServer, shutdownServer, waitFire } from '../utils.js'; + +describe('Mute', () => { + let p: childProcess.ChildProcess; + + // alice mutes carol + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + it('ミュート作成', async () => { + const res = await request('/mute/create', { + userId: carol.id, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice hi' }); + const carolNote = await post(carol, { text: '@alice hi' }); + + const res = await request('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + await post(carol, { text: '@alice hi' }); + + const res = await request('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); + + assert.strictEqual(fired, false); + }); + + it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + await request('/notifications/mark-all-as-read', {}, alice); + + const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); + + assert.strictEqual(fired, false); + }); + + describe('Timeline', () => { + it('タイムラインにミュートしているユーザーの投稿が含まれない', async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { + const aliceNote = await post(alice); + const carolNote = await post(carol); + const bobNote = await post(bob, { + renoteId: carolNote.id, + }); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + }); + + describe('Notification', () => { + it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { + const aliceNote = await post(alice); + await react(bob, aliceNote, 'like'); + await react(carol, aliceNote, 'like'); + + const res = await request('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + }); + }); +}); diff --git a/packages/backend/test/_e2e/note.ts b/packages/backend/test/_e2e/note.ts new file mode 100644 index 0000000000..d75a5c8285 --- /dev/null +++ b/packages/backend/test/_e2e/note.ts @@ -0,0 +1,370 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { Note } from '../../src/models/entities/note.js'; +import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from '../utils.js'; + +describe('Note', () => { + let p: childProcess.ChildProcess; + let Notes: any; + + let alice: any; + let bob: any; + + beforeAll(async () => { + p = await startServer(); + const connection = await initTestDb(true); + Notes = connection.getRepository(Note); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + it('投稿できる', async () => { + const post = { + text: 'test', + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + }); + + it('ファイルを添付できる', async () => { + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + + const res = await request('/notes/create', { + fileIds: [file.id], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); + }, 1000 * 10); + + it('他人のファイルは無視', async () => { + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + + const res = await request('/notes/create', { + text: 'test', + fileIds: [file.id], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + }, 1000 * 10); + + it('存在しないファイルは無視', async () => { + const res = await request('/notes/create', { + text: 'test', + fileIds: ['000000000000000000000000'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + }); + + it('不正なファイルIDは無視', async () => { + const res = await request('/notes/create', { + fileIds: ['kyoppie'], + }, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + }); + + it('返信できる', async () => { + const bobPost = await post(bob, { + text: 'foo', + }); + + const alicePost = { + text: 'bar', + replyId: bobPost.id, + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); + assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); + }); + + it('renoteできる', async () => { + const bobPost = await post(bob, { + text: 'test', + }); + + const alicePost = { + renoteId: bobPost.id, + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + }); + + it('引用renoteできる', async () => { + const bobPost = await post(bob, { + text: 'test', + }); + + const alicePost = { + text: 'test', + renoteId: bobPost.id, + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + }); + + it('文字数ぎりぎりで怒られない', async () => { + const post = { + text: '!'.repeat(3000), + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 200); + }); + + it('文字数オーバーで怒られる', async () => { + const post = { + text: '!'.repeat(3001), + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + it('存在しないリプライ先で怒られる', async () => { + const post = { + text: 'test', + replyId: '000000000000000000000000', + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + it('存在しないrenote対象で怒られる', async () => { + const post = { + renoteId: '000000000000000000000000', + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + it('不正なリプライ先IDで怒られる', async () => { + const post = { + text: 'test', + replyId: 'foo', + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + it('不正なrenote対象IDで怒られる', async () => { + const post = { + renoteId: 'foo', + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + it('存在しないユーザーにメンションできる', async () => { + const post = { + text: '@ghost yo', + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + }); + + it('同じユーザーに複数メンションしても内部的にまとめられる', async () => { + const post = { + text: '@bob @bob @bob yo', + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + + const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); + assert.deepStrictEqual(noteDoc.mentions, [bob.id]); + }); + + describe('notes/create', () => { + it('投票を添付できる', async () => { + const res = await request('/notes/create', { + text: 'test', + poll: { + choices: ['foo', 'bar'], + }, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.poll != null, true); + }); + + it('投票の選択肢が無くて怒られる', async () => { + const res = await request('/notes/create', { + poll: {}, + }, alice); + assert.strictEqual(res.status, 400); + }); + + it('投票の選択肢が無くて怒られる (空の配列)', async () => { + const res = await request('/notes/create', { + poll: { + choices: [], + }, + }, alice); + assert.strictEqual(res.status, 400); + }); + + it('投票の選択肢が1つで怒られる', async () => { + const res = await request('/notes/create', { + poll: { + choices: ['Strawberry Pasta'], + }, + }, alice); + assert.strictEqual(res.status, 400); + }); + + it('投票できる', async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + it('複数投票できない', async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0, + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + it('許可されている場合は複数投票できる', async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + multiple: true, + }, + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0, + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + it('締め切られている場合は投票できない', async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + expiredAfter: 1, + }, + }, alice); + + await new Promise(x => setTimeout(x, 2)); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/delete', () => { + it('delete a reply', async () => { + const mainNoteRes = await api('notes/create', { + text: 'main post', + }, alice); + const replyOneRes = await api('notes/create', { + text: 'reply one', + replyId: mainNoteRes.body.createdNote.id, + }, alice); + const replyTwoRes = await api('notes/create', { + text: 'reply two', + replyId: mainNoteRes.body.createdNote.id, + }, alice); + + const deleteOneRes = await api('notes/delete', { + noteId: replyOneRes.body.createdNote.id, + }, alice); + + assert.strictEqual(deleteOneRes.status, 204); + let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.strictEqual(mainNote.repliesCount, 1); + + const deleteTwoRes = await api('notes/delete', { + noteId: replyTwoRes.body.createdNote.id, + }, alice); + + assert.strictEqual(deleteTwoRes.status, 204); + mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.strictEqual(mainNote.repliesCount, 0); + }); + }); +}); diff --git a/packages/backend/test/_e2e/streaming.ts b/packages/backend/test/_e2e/streaming.ts new file mode 100644 index 0000000000..4dad322e99 --- /dev/null +++ b/packages/backend/test/_e2e/streaming.ts @@ -0,0 +1,545 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { Following } from '../../src/models/entities/following.js'; +import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from '../utils.js'; + +describe('Streaming', () => { + let p: childProcess.ChildProcess; + let Followings: any; + + const follow = async (follower: any, followee: any) => { + await Followings.save({ + id: 'a', + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + followerHost: follower.host, + followerInbox: null, + followerSharedInbox: null, + followeeHost: followee.host, + followeeInbox: null, + followeeSharedInbox: null, + }); + }; + + describe('Streaming', () => { + // Local users + let ayano: any; + let kyoko: any; + let chitose: any; + + // Remote users + let akari: any; + let chinatsu: any; + + let kyokoNote: any; + let list: any; + + beforeAll(async () => { + p = await startServer(); + const connection = await initTestDb(true); + Followings = connection.getRepository(Following); + + ayano = await signup({ username: 'ayano' }); + kyoko = await signup({ username: 'kyoko' }); + chitose = await signup({ username: 'chitose' }); + + akari = await signup({ username: 'akari', host: 'example.com' }); + chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); + + kyokoNote = await post(kyoko, { text: 'foo' }); + + // Follow: ayano => kyoko + await api('following/create', { userId: kyoko.id }, ayano); + + // Follow: ayano => akari + await follow(ayano, akari); + + // List: chitose => ayano, kyoko + list = await api('users/lists/create', { + name: 'my list', + }, chitose).then(x => x.body); + + await api('users/lists/push', { + listId: list.id, + userId: ayano.id, + }, chitose); + + await api('users/lists/push', { + listId: list.id, + userId: kyoko.id, + }, chitose); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + describe('Events', () => { + it('mention event', async () => { + const fired = await waitFire( + kyoko, 'main', // kyoko:main + () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko + msg => msg.type === 'mention' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + it('renote event', async () => { + const fired = await waitFire( + kyoko, 'main', // kyoko:main + () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote + msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id, // wait renote + ); + + assert.strictEqual(fired, true); + }); + }); + + describe('Home Timeline', () => { + it('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:Home + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしているユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないユーザーの投稿は流れない', async () => { + const fired = await waitFire( + kyoko, 'homeTimeline', // kyoko:home + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + }); // Home + + describe('Local Timeline', () => { + it('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + it('リモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, akari), // akari posts + msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari + ); + + assert.strictEqual(fired, false); + }); + + it('ホーム指定の投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Hybrid Timeline', () => { + it('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしているリモートユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, akari), // akari posts + msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないリモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしているユーザーのホーム投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Global Timeline', () => { + it('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないリモートユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, true); + }); + + it('ホーム投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('UserList Timeline', () => { + it('リストに入れているユーザーの投稿が流れる', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, ayano), + msg => msg.type === 'note' && msg.body.userId === ayano.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, true); + }); + + it('リストに入れていないユーザーの投稿は流れない', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, chinatsu), + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #4471 + it('リストに入れているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano), + msg => msg.type === 'note' && msg.body.userId === ayano.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, true); + }); + + // #4335 + it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Hashtag Timeline', () => { + it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + assert.deepStrictEqual(body.text, '#foo'); + ws.close(); + done(); + } + }, { + q: [ + ['foo'], + ], + }); + + post(chitose, { + text: '#foo', + }); + })); + + it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + } + }, { + q: [ + ['foo', 'bar'], + ], + }); + + post(chitose, { + text: '#foo', + }); + + post(chitose, { + text: '#bar', + }); + + post(chitose, { + text: '#foo #bar', + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + ws.close(); + done(); + }, 3000); + })); + + it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + } + }, { + q: [ + ['foo'], + ['bar'], + ], + }); + + post(chitose, { + text: '#foo', + }); + + post(chitose, { + text: '#bar', + }); + + post(chitose, { + text: '#foo #bar', + }); + + post(chitose, { + text: '#piyo', + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 1); + assert.strictEqual(barCount, 1); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 0); + ws.close(); + done(); + }, 3000); + })); + + it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + let waaaCount = 0; + + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + if (body.text === '#waaa') waaaCount++; + } + }, { + q: [ + ['foo', 'bar'], + ['piyo'], + ], + }); + + post(chitose, { + text: '#foo', + }); + + post(chitose, { + text: '#bar', + }); + + post(chitose, { + text: '#foo #bar', + }); + + post(chitose, { + text: '#piyo', + }); + + post(chitose, { + text: '#waaa', + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 1); + assert.strictEqual(waaaCount, 0); + ws.close(); + done(); + }, 3000); + })); + }); + }); +}); diff --git a/packages/backend/test/_e2e/thread-mute.ts b/packages/backend/test/_e2e/thread-mute.ts new file mode 100644 index 0000000000..0ed9aa0666 --- /dev/null +++ b/packages/backend/test/_e2e/thread-mute.ts @@ -0,0 +1,103 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, react, connectStream, startServer, shutdownServer } from '../utils.js'; + +describe('Note thread mute', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 30); + + afterAll(async () => { + await shutdownServer(p); + }); + + it('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await request('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); + }); + + it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + const res = await request('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', async ({ type, body }) => { + if (type === 'unreadMention') { + if (body === bobNote.id) return; + fired = true; + } + }); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + it('i/notifications にミュートしているスレッドの通知が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await request('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); + + // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい + }); +}); diff --git a/packages/backend/test/_e2e/user-notes.ts b/packages/backend/test/_e2e/user-notes.ts new file mode 100644 index 0000000000..353875634c --- /dev/null +++ b/packages/backend/test/_e2e/user-notes.ts @@ -0,0 +1,61 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { signup, request, post, uploadUrl, startServer, shutdownServer } from '../utils.js'; + +describe('users/notes', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); + jpgNote = await post(alice, { + fileIds: [jpg.id], + }); + pngNote = await post(alice, { + fileIds: [png.id], + }); + jpgPngNote = await post(alice, { + fileIds: [jpg.id, png.id], + }); + }, 1000 * 30); + + afterAll(async() => { + await shutdownServer(p); + }); + + it('ファイルタイプ指定 (jpg)', async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 2); + assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + }); + + it('ファイルタイプ指定 (jpg or png)', async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg', 'image/png'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 3); + assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + }); +}); diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts deleted file mode 100644 index f4ae27e5ec..0000000000 --- a/packages/backend/test/activitypub.ts +++ /dev/null @@ -1,96 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import rndstr from 'rndstr'; -import { initDb } from '../src/db/postgre.js'; -import { initTestDb } from './utils.js'; - -describe('ActivityPub', () => { - before(async () => { - //await initTestDb(); - await initDb(); - }); - - describe('Parse minimum object', () => { - const host = 'https://host1.test'; - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const actor = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: actorId, - type: 'Person', - preferredUsername, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; - - const post = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${host}/users/${rndstr('0-9a-z', 8)}`, - type: 'Note', - attributedTo: actor.id, - to: 'https://www.w3.org/ns/activitystreams#Public', - content: 'あ', - }; - - it('Minimum Actor', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createPerson } = await import('../src/remote/activitypub/models/person.js'); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - - const user = await createPerson(actor.id, resolver); - - assert.deepStrictEqual(user.uri, actor.id); - assert.deepStrictEqual(user.username, actor.preferredUsername); - assert.deepStrictEqual(user.inbox, actor.inbox); - }); - - it('Minimum Note', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createNote } = await import('../src/remote/activitypub/models/note.js'); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - resolver._register(post.id, post); - - const note = await createNote(post.id, resolver, true); - - assert.deepStrictEqual(note?.uri, post.id); - assert.deepStrictEqual(note.visibility, 'public'); - assert.deepStrictEqual(note.text, post.content); - }); - }); - - describe('Truncate long name', () => { - const host = 'https://host1.test'; - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const name = rndstr('0-9a-z', 129); - - const actor = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: actorId, - type: 'Person', - preferredUsername, - name, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; - - it('Actor', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createPerson } = await import('../src/remote/activitypub/models/person.js'); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - - const user = await createPerson(actor.id, resolver); - - assert.deepStrictEqual(user.name, actor.name.substr(0, 128)); - }); - }); -}); diff --git a/packages/backend/test/ap-request.ts b/packages/backend/test/ap-request.ts deleted file mode 100644 index da95c421f3..0000000000 --- a/packages/backend/test/ap-request.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as assert from 'assert'; -import httpSignature from 'http-signature'; -import { genRsaKeyPair } from '../src/misc/gen-key-pair.js'; -import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js'; - -export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { - return { - scheme: 'Signature', - params: { - keyId: 'KeyID', // dummy, not used for verify - algorithm: algorithm, - headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify - signature: signature, - }, - signingString: signingString, - algorithm: algorithm.toUpperCase(), - keyId: 'KeyID', // dummy, not used for verify - }; -}; - -describe('ap-request', () => { - it('createSignedPost with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/inbox'; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - 'User-Agent': 'UA', - }; - - const req = createSignedPost({ key, url, body, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); - - it('createSignedGet with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/outbox'; - const headers = { - 'User-Agent': 'UA', - }; - - const req = createSignedGet({ key, url, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); -}); diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts deleted file mode 100644 index b155549f98..0000000000 --- a/packages/backend/test/api-visibility.ts +++ /dev/null @@ -1,476 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, startServer, shutdownServer } from './utils.js'; - -describe('API visibility', () => { - let p: childProcess.ChildProcess; - - before(async () => { - p = await startServer(); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('Note visibility', async () => { - //#region vars - /** ヒロイン */ - let alice: any; - /** フォロワー */ - let follower: any; - /** 非フォロワー */ - let other: any; - /** 非フォロワーでもリプライやメンションをされた人 */ - let target: any; - /** specified mentionでmentionを飛ばされる人 */ - let target2: any; - - /** public-post */ - let pub: any; - /** home-post */ - let home: any; - /** followers-post */ - let fol: any; - /** specified-post */ - let spe: any; - - /** public-reply to target's post */ - let pubR: any; - /** home-reply to target's post */ - let homeR: any; - /** followers-reply to target's post */ - let folR: any; - /** specified-reply to target's post */ - let speR: any; - - /** public-mention to target */ - let pubM: any; - /** home-mention to target */ - let homeM: any; - /** followers-mention to target */ - let folM: any; - /** specified-mention to target */ - let speM: any; - - /** reply target post */ - let tgt: any; - //#endregion - - const show = async (noteId: any, by: any) => { - return await request('/notes/show', { - noteId, - }, by); - }; - - before(async () => { - //#region prepare - // signup - alice = await signup({ username: 'alice' }); - follower = await signup({ username: 'follower' }); - other = await signup({ username: 'other' }); - target = await signup({ username: 'target' }); - target2 = await signup({ username: 'target2' }); - - // follow alice <= follower - await request('/following/create', { userId: alice.id }, follower); - - // normal posts - pub = await post(alice, { text: 'x', visibility: 'public' }); - home = await post(alice, { text: 'x', visibility: 'home' }); - fol = await post(alice, { text: 'x', visibility: 'followers' }); - spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); - - // replies - tgt = await post(target, { text: 'y', visibility: 'public' }); - pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); - homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); - folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); - speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); - - // mentions - pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); - homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); - folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); - speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); - //#endregion - }); - - //#region show post - // public - it('[show] public-postを自分が見れる', async(async () => { - const res = await show(pub.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-postをフォロワーが見れる', async(async () => { - const res = await show(pub.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-postを非フォロワーが見れる', async(async () => { - const res = await show(pub.id, other); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-postを未認証が見れる', async(async () => { - const res = await show(pub.id, null); - assert.strictEqual(res.body.text, 'x'); - })); - - // home - it('[show] home-postを自分が見れる', async(async () => { - const res = await show(home.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-postをフォロワーが見れる', async(async () => { - const res = await show(home.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-postを非フォロワーが見れる', async(async () => { - const res = await show(home.id, other); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-postを未認証が見れる', async(async () => { - const res = await show(home.id, null); - assert.strictEqual(res.body.text, 'x'); - })); - - // followers - it('[show] followers-postを自分が見れる', async(async () => { - const res = await show(fol.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] followers-postをフォロワーが見れる', async(async () => { - const res = await show(fol.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] followers-postを非フォロワーが見れない', async(async () => { - const res = await show(fol.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] followers-postを未認証が見れない', async(async () => { - const res = await show(fol.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - - // specified - it('[show] specified-postを自分が見れる', async(async () => { - const res = await show(spe.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-postを指定ユーザーが見れる', async(async () => { - const res = await show(spe.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-postをフォロワーが見れない', async(async () => { - const res = await show(spe.id, follower); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-postを非フォロワーが見れない', async(async () => { - const res = await show(spe.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-postを未認証が見れない', async(async () => { - const res = await show(spe.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - //#endregion - - //#region show reply - // public - it('[show] public-replyを自分が見れる', async(async () => { - const res = await show(pubR.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-replyをされた人が見れる', async(async () => { - const res = await show(pubR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-replyをフォロワーが見れる', async(async () => { - const res = await show(pubR.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-replyを非フォロワーが見れる', async(async () => { - const res = await show(pubR.id, other); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] public-replyを未認証が見れる', async(async () => { - const res = await show(pubR.id, null); - assert.strictEqual(res.body.text, 'x'); - })); - - // home - it('[show] home-replyを自分が見れる', async(async () => { - const res = await show(homeR.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-replyをされた人が見れる', async(async () => { - const res = await show(homeR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-replyをフォロワーが見れる', async(async () => { - const res = await show(homeR.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-replyを非フォロワーが見れる', async(async () => { - const res = await show(homeR.id, other); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] home-replyを未認証が見れる', async(async () => { - const res = await show(homeR.id, null); - assert.strictEqual(res.body.text, 'x'); - })); - - // followers - it('[show] followers-replyを自分が見れる', async(async () => { - const res = await show(folR.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => { - const res = await show(folR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] followers-replyをフォロワーが見れる', async(async () => { - const res = await show(folR.id, follower); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] followers-replyを非フォロワーが見れない', async(async () => { - const res = await show(folR.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] followers-replyを未認証が見れない', async(async () => { - const res = await show(folR.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - - // specified - it('[show] specified-replyを自分が見れる', async(async () => { - const res = await show(speR.id, alice); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-replyを指定ユーザーが見れる', async(async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-replyをフォロワーが見れない', async(async () => { - const res = await show(speR.id, follower); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-replyを非フォロワーが見れない', async(async () => { - const res = await show(speR.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-replyを未認証が見れない', async(async () => { - const res = await show(speR.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - //#endregion - - //#region show mention - // public - it('[show] public-mentionを自分が見れる', async(async () => { - const res = await show(pubM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] public-mentionをされた人が見れる', async(async () => { - const res = await show(pubM.id, target); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] public-mentionをフォロワーが見れる', async(async () => { - const res = await show(pubM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] public-mentionを非フォロワーが見れる', async(async () => { - const res = await show(pubM.id, other); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] public-mentionを未認証が見れる', async(async () => { - const res = await show(pubM.id, null); - assert.strictEqual(res.body.text, '@target x'); - })); - - // home - it('[show] home-mentionを自分が見れる', async(async () => { - const res = await show(homeM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] home-mentionをされた人が見れる', async(async () => { - const res = await show(homeM.id, target); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] home-mentionをフォロワーが見れる', async(async () => { - const res = await show(homeM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] home-mentionを非フォロワーが見れる', async(async () => { - const res = await show(homeM.id, other); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] home-mentionを未認証が見れる', async(async () => { - const res = await show(homeM.id, null); - assert.strictEqual(res.body.text, '@target x'); - })); - - // followers - it('[show] followers-mentionを自分が見れる', async(async () => { - const res = await show(folM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async(async () => { - const res = await show(folM.id, target); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] followers-mentionをフォロワーが見れる', async(async () => { - const res = await show(folM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - })); - - it('[show] followers-mentionを非フォロワーが見れない', async(async () => { - const res = await show(folM.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] followers-mentionを未認証が見れない', async(async () => { - const res = await show(folM.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - - // specified - it('[show] specified-mentionを自分が見れる', async(async () => { - const res = await show(speM.id, alice); - assert.strictEqual(res.body.text, '@target2 x'); - })); - - it('[show] specified-mentionを指定ユーザーが見れる', async(async () => { - const res = await show(speM.id, target); - assert.strictEqual(res.body.text, '@target2 x'); - })); - - it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => { - const res = await show(speM.id, target2); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-mentionをフォロワーが見れない', async(async () => { - const res = await show(speM.id, follower); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-mentionを非フォロワーが見れない', async(async () => { - const res = await show(speM.id, other); - assert.strictEqual(res.body.isHidden, true); - })); - - it('[show] specified-mentionを未認証が見れない', async(async () => { - const res = await show(speM.id, null); - assert.strictEqual(res.body.isHidden, true); - })); - //#endregion - - //#region HTL - it('[HTL] public-post が 自分が見れる', async(async () => { - const res = await request('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); - assert.strictEqual(notes[0].text, 'x'); - })); - - it('[HTL] public-post が 非フォロワーから見れない', async(async () => { - const res = await request('/notes/timeline', { limit: 100 }, other); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); - assert.strictEqual(notes.length, 0); - })); - - it('[HTL] followers-post が フォロワーから見れる', async(async () => { - const res = await request('/notes/timeline', { limit: 100 }, follower); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == fol.id); - assert.strictEqual(notes[0].text, 'x'); - })); - //#endregion - - //#region RTL - it('[replies] followers-reply が フォロワーから見れる', async(async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, 'x'); - })); - - it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes.length, 0); - })); - - it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, 'x'); - })); - //#endregion - - //#region MTL - it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, 'x'); - })); - - it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folM.id); - assert.strictEqual(notes[0].text, '@target x'); - })); - //#endregion - }); -}); diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.ts deleted file mode 100644 index b1b2ecafc7..0000000000 --- a/packages/backend/test/api.ts +++ /dev/null @@ -1,83 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; - -describe('API', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('General validation', () => { - it('wrong type', async(async () => { - const res = await request('/test', { - required: true, - string: 42, - }); - assert.strictEqual(res.status, 400); - })); - - it('missing require param', async(async () => { - const res = await request('/test', { - string: 'a', - }); - assert.strictEqual(res.status, 400); - })); - - it('invalid misskey:id (empty string)', async(async () => { - const res = await request('/test', { - required: true, - id: '', - }); - assert.strictEqual(res.status, 400); - })); - - it('valid misskey:id', async(async () => { - const res = await request('/test', { - required: true, - id: '8wvhjghbxu', - }); - assert.strictEqual(res.status, 200); - })); - - it('default value', async(async () => { - const res = await request('/test', { - required: true, - string: 'a', - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.default, 'hello'); - })); - - it('can set null even if it has default value', async(async () => { - const res = await request('/test', { - required: true, - nullableDefault: null, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, null); - })); - - it('cannot set undefined if it has default value', async(async () => { - const res = await request('/test', { - required: true, - nullableDefault: undefined, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, 'hello'); - })); - }); -}); diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.ts deleted file mode 100644 index b3343813cd..0000000000 --- a/packages/backend/test/block.ts +++ /dev/null @@ -1,85 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, startServer, shutdownServer } from './utils.js'; - -describe('Block', () => { - let p: childProcess.ChildProcess; - - // alice blocks bob - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('Block作成', async(async () => { - const res = await request('/blocking/create', { - userId: bob.id, - }, alice); - - assert.strictEqual(res.status, 200); - })); - - it('ブロックされているユーザーをフォローできない', async(async () => { - const res = await request('/following/create', { userId: alice.id }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); - })); - - it('ブロックされているユーザーにリアクションできない', async(async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); - })); - - it('ブロックされているユーザーに返信できない', async(async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); - })); - - it('ブロックされているユーザーのノートをRenoteできない', async(async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); - })); - - // TODO: ユーザーリストに入れられないテスト - - // TODO: ユーザーリストから除外されるテスト - - it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async(async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request('/notes/local-timeline', {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - })); -}); diff --git a/packages/backend/test/chart.ts b/packages/backend/test/chart.ts deleted file mode 100644 index ac0844679f..0000000000 --- a/packages/backend/test/chart.ts +++ /dev/null @@ -1,531 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as lolex from '@sinonjs/fake-timers'; -import TestChart from '../src/services/chart/charts/test.js'; -import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; -import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; -import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; -import { initDb } from '../src/db/postgre.js'; - -describe('Chart', () => { - let testChart: TestChart; - let testGroupedChart: TestGroupedChart; - let testUniqueChart: TestUniqueChart; - let testIntersectionChart: TestIntersectionChart; - let clock: lolex.InstalledClock; - - beforeEach(async () => { - await initDb(true); - - testChart = new TestChart(); - testGroupedChart = new TestGroupedChart(); - testUniqueChart = new TestUniqueChart(); - testIntersectionChart = new TestIntersectionChart(); - - clock = lolex.install({ - now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - shouldClearNativeTimers: true, - }); - }); - - afterEach(() => { - clock.uninstall(); - }); - - it('Can updates', async () => { - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it('Can updates (dec)', async () => { - await testChart.decrement(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [1, 0, 0], - inc: [0, 0, 0], - total: [-1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [1, 0, 0], - inc: [0, 0, 0], - total: [-1, 0, 0], - }, - }); - }); - - it('Empty chart', async () => { - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - }); - - it('Can updates at multiple times at same time', async () => { - await testChart.increment(); - await testChart.increment(); - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [3, 0, 0], - total: [3, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [3, 0, 0], - total: [3, 0, 0], - }, - }); - }); - - it('複数回saveされてもデータの更新は一度だけ', async () => { - await testChart.increment(); - await testChart.save(); - await testChart.save(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it('Can updates at different times', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('01:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 1, 0], - total: [2, 1, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - // 仕様上はこうなってほしいけど、実装は難しそうなのでskip - /* - it('Can updates at different times without save', async () => { - await testChart.increment(); - - clock.tick('01:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 1, 0], - total: [2, 1, 0] - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0] - }, - }); - }); - */ - - it('Can padding', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('02:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 1], - total: [2, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - // 要求された範囲にログがひとつもない場合でもパディングできる - it('Can padding from past range', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('05:00:00'); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる - // Issue #3190 - it('Can padding from past range 2', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('05:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [2, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - it('Can specify offset', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('01:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); - const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - it('Can specify offset (floor time)', async () => { - clock.tick('00:30:00'); - - await testChart.increment(); - await testChart.save(); - - clock.tick('01:30:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); - const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - describe('Grouped', () => { - it('Can updates', async () => { - await testGroupedChart.increment('alice'); - await testGroupedChart.save(); - - const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice'); - const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice'); - const bobChartHours = await testGroupedChart.getChart('hour', 3, null, 'bob'); - const bobChartDays = await testGroupedChart.getChart('day', 3, null, 'bob'); - - assert.deepStrictEqual(aliceChartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(aliceChartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(bobChartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - - assert.deepStrictEqual(bobChartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - }); - }); - - describe('Unique increment', () => { - it('Can updates', async () => { - await testUniqueChart.uniqueIncrement('alice'); - await testUniqueChart.uniqueIncrement('alice'); - await testUniqueChart.uniqueIncrement('bob'); - await testUniqueChart.save(); - - const chartHours = await testUniqueChart.getChart('hour', 3, null); - const chartDays = await testUniqueChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: [2, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - foo: [2, 0, 0], - }); - }); - - describe('Intersection', () => { - it('条件が満たされていない場合はカウントされない', async () => { - await testIntersectionChart.addA('alice'); - await testIntersectionChart.addA('bob'); - await testIntersectionChart.addB('carol'); - await testIntersectionChart.save(); - - const chartHours = await testIntersectionChart.getChart('hour', 3, null); - const chartDays = await testIntersectionChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - a: [2, 0, 0], - b: [1, 0, 0], - aAndB: [0, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - a: [2, 0, 0], - b: [1, 0, 0], - aAndB: [0, 0, 0], - }); - }); - - it('条件が満たされている場合にカウントされる', async () => { - await testIntersectionChart.addA('alice'); - await testIntersectionChart.addA('bob'); - await testIntersectionChart.addB('carol'); - await testIntersectionChart.addB('alice'); - await testIntersectionChart.save(); - - const chartHours = await testIntersectionChart.getChart('hour', 3, null); - const chartDays = await testIntersectionChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - a: [2, 0, 0], - b: [2, 0, 0], - aAndB: [1, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - a: [2, 0, 0], - b: [2, 0, 0], - aAndB: [1, 0, 0], - }); - }); - }); - }); - - describe('Resync', () => { - it('Can resync', async () => { - testChart.total = 1; - - await testChart.resync(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it('Can resync (2)', async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick('01:00:00'); - - testChart.total = 100; - - await testChart.resync(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 1, 0], - total: [100, 1, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [100, 0, 0], - }, - }); - }); - }); -}); diff --git a/packages/backend/test/endpoints.ts b/packages/backend/test/endpoints.ts deleted file mode 100644 index 2aedc25f2c..0000000000 --- a/packages/backend/test/endpoints.ts +++ /dev/null @@ -1,865 +0,0 @@ -/* -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; - -describe('API: Endpoints', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('signup', () => { - it('不正なユーザー名でアカウントが作成できない', async(async () => { - const res = await request('/signup', { - username: 'test.', - password: 'test' - }); - assert.strictEqual(res.status, 400); - })); - - it('空のパスワードでアカウントが作成できない', async(async () => { - const res = await request('/signup', { - username: 'test', - password: '' - }); - assert.strictEqual(res.status, 400); - })); - - it('正しくアカウントが作成できる', async(async () => { - const me = { - username: 'test1', - password: 'test1' - }; - - const res = await request('/signup', me); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.username, me.username); - })); - - it('同じユーザー名のアカウントは作成できない', async(async () => { - await signup({ - username: 'test2' - }); - - const res = await request('/signup', { - username: 'test2', - password: 'test2' - }); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('signin', () => { - it('間違ったパスワードでサインインできない', async(async () => { - await signup({ - username: 'test3', - password: 'foo' - }); - - const res = await request('/signin', { - username: 'test3', - password: 'bar' - }); - - assert.strictEqual(res.status, 403); - })); - - it('クエリをインジェクションできない', async(async () => { - await signup({ - username: 'test4' - }); - - const res = await request('/signin', { - username: 'test4', - password: { - $gt: '' - } - }); - - assert.strictEqual(res.status, 400); - })); - - it('正しい情報でサインインできる', async(async () => { - await signup({ - username: 'test5', - password: 'foo' - }); - - const res = await request('/signin', { - username: 'test5', - password: 'foo' - }); - - assert.strictEqual(res.status, 200); - })); - }); - - describe('i/update', () => { - it('アカウント設定を更新できる', async(async () => { - const myName = '大室櫻子'; - const myLocation = '七森中'; - const myBirthday = '2000-09-07'; - - const res = await request('/i/update', { - name: myName, - location: myLocation, - birthday: myBirthday - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, myName); - assert.strictEqual(res.body.location, myLocation); - assert.strictEqual(res.body.birthday, myBirthday); - })); - - it('名前を空白にできない', async(async () => { - const res = await request('/i/update', { - name: ' ' - }, alice); - assert.strictEqual(res.status, 400); - })); - - it('誕生日の設定を削除できる', async(async () => { - await request('/i/update', { - birthday: '2000-09-07' - }, alice); - - const res = await request('/i/update', { - birthday: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.birthday, null); - })); - - it('不正な誕生日の形式で怒られる', async(async () => { - const res = await request('/i/update', { - birthday: '2000/09/07' - }, alice); - assert.strictEqual(res.status, 400); - })); - }); - - describe('users/show', () => { - it('ユーザーが取得できる', async(async () => { - const res = await request('/users/show', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, alice.id); - })); - - it('ユーザーが存在しなかったら怒る', async(async () => { - const res = await request('/users/show', { - userId: '000000000000000000000000' - }); - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/users/show', { - userId: 'kyoppie' - }); - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/show', () => { - it('投稿が取得できる', async(async () => { - const myPost = await post(alice, { - text: 'test' - }); - - const res = await request('/notes/show', { - noteId: myPost.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, myPost.id); - assert.strictEqual(res.body.text, myPost.text); - })); - - it('投稿が存在しなかったら怒る', async(async () => { - const res = await request('/notes/show', { - noteId: '000000000000000000000000' - }); - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/notes/show', { - noteId: 'kyoppie' - }); - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/reactions/create', () => { - it('リアクションできる', async(async () => { - const bobPost = await post(bob); - - const alice = await signup({ username: 'alice' }); - const res = await request('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - - const resNote = await request('/notes/show', { - noteId: bobPost.id, - }, alice); - - assert.strictEqual(resNote.status, 200); - assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); - })); - - it('自分の投稿にもリアクションできる', async(async () => { - const myPost = await post(alice); - - const res = await request('/notes/reactions/create', { - noteId: myPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - })); - - it('二重にリアクションできない', async(async () => { - const bobPost = await post(bob); - - await react(alice, bobPost, 'like'); - - const res = await request('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しない投稿にはリアクションできない', async(async () => { - const res = await request('/notes/reactions/create', { - noteId: '000000000000000000000000', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/notes/reactions/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/notes/reactions/create', { - noteId: 'kyoppie', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('following/create', () => { - it('フォローできる', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 200); - })); - - it('既にフォローしている場合は怒る', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーはフォローできない', async(async () => { - const res = await request('/following/create', { - userId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('自分自身はフォローできない', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/following/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/following/create', { - userId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('following/delete', () => { - it('フォロー解除できる', async(async () => { - await request('/following/create', { - userId: alice.id - }, bob); - - const res = await request('/following/delete', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 200); - })); - - it('フォローしていない場合は怒る', async(async () => { - const res = await request('/following/delete', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーはフォロー解除できない', async(async () => { - const res = await request('/following/delete', { - userId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('自分自身はフォロー解除できない', async(async () => { - const res = await request('/following/delete', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/following/delete', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/following/delete', { - userId: 'kyoppie' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('drive', () => { - it('ドライブ情報を取得できる', async(async () => { - await uploadFile({ - userId: alice.id, - size: 256 - }); - await uploadFile({ - userId: alice.id, - size: 512 - }); - await uploadFile({ - userId: alice.id, - size: 1024 - }); - const res = await request('/drive', {}, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - expect(res.body).have.property('usage').eql(1792); - })); - }); - - describe('drive/files/create', () => { - it('ファイルを作成できる', async(async () => { - const res = await uploadFile(alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'Lenna.png'); - })); - - it('ファイルに名前を付けられる', async(async () => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .field('name', 'Belmond.png') - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Belmond.png'); - })); - - it('ファイル無しで怒られる', async(async () => { - const res = await request('/drive/files/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('SVGファイルを作成できる', async(async () => { - const res = await uploadFile(alice, __dirname + '/resources/image.svg'); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'image.svg'); - assert.strictEqual(res.body.type, 'image/svg+xml'); - })); - }); - - describe('drive/files/update', () => { - it('名前を更新できる', async(async () => { - const file = await uploadFile(alice); - const newName = 'いちごパスタ.png'; - - const res = await request('/drive/files/update', { - fileId: file.id, - name: newName - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, newName); - })); - - it('他人のファイルは更新できない', async(async () => { - const file = await uploadFile(bob); - - const res = await request('/drive/files/update', { - fileId: file.id, - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('親フォルダを更新できる', async(async () => { - const file = await uploadFile(alice); - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, folder.id); - })); - - it('親フォルダを無しにできる', async(async () => { - const file = await uploadFile(alice); - - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, null); - })); - - it('他人のフォルダには入れられない', async(async () => { - const file = await uploadFile(alice); - const folder = (await request('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないフォルダで怒られる', async(async () => { - const file = await uploadFile(alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なフォルダIDで怒られる', async(async () => { - const file = await uploadFile(alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('ファイルが存在しなかったら怒る', async(async () => { - const res = await request('/drive/files/update', { - fileId: '000000000000000000000000', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/drive/files/update', { - fileId: 'kyoppie', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('drive/folders/create', () => { - it('フォルダを作成できる', async(async () => { - const res = await request('/drive/folders/create', { - name: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'test'); - })); - }); - - describe('drive/folders/update', () => { - it('名前を更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'new name'); - })); - - it('他人のフォルダを更新できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('親フォルダを更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, parentFolder.id); - })); - - it('親フォルダを無しに更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, null); - })); - - it('他人のフォルダを親フォルダに設定できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, bob)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await request('/drive/folders/update', { - folderId: parentFolder.id, - parentId: folder.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない(再帰的)', async(async () => { - const folderA = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderB = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderC = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - await request('/drive/folders/update', { - folderId: folderB.id, - parentId: folderA.id - }, alice); - await request('/drive/folders/update', { - folderId: folderC.id, - parentId: folderB.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folderA.id, - parentId: folderC.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない(自身)', async(async () => { - const folderA = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folderA.id, - parentId: folderA.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しない親フォルダを設定できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正な親フォルダIDで怒られる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないフォルダを更新できない', async(async () => { - const res = await request('/drive/folders/update', { - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なフォルダIDで怒られる', async(async () => { - const res = await request('/drive/folders/update', { - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('messaging/messages/create', () => { - it('メッセージを送信できる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id, - text: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.text, 'test'); - })); - - it('自分自身にはメッセージを送信できない', async(async () => { - const res = await request('/messaging/messages/create', { - userId: alice.id, - text: 'Yo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーにはメッセージを送信できない', async(async () => { - const res = await request('/messaging/messages/create', { - userId: '000000000000000000000000', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なユーザーIDで怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: 'foo', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('テキストが無くて怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('文字数オーバーで怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id, - text: '!'.repeat(1001) - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/replies', () => { - it('自分に閲覧権限のない投稿は含まれない', async(async () => { - const alicePost = await post(alice, { - text: 'foo' - }); - - await post(bob, { - replyId: alicePost.id, - text: 'bar', - visibility: 'specified', - visibleUserIds: [alice.id] - }); - - const res = await request('/notes/replies', { - noteId: alicePost.id - }, carol); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 0); - })); - }); - - describe('notes/timeline', () => { - it('フォロワー限定投稿が含まれる', async(async () => { - await request('/following/create', { - userId: alice.id - }, bob); - - const alicePost = await post(alice, { - text: 'foo', - visibility: 'followers' - }); - - const res = await request('/notes/timeline', {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 1); - assert.strictEqual(res.body[0].id, alicePost.id); - })); - }); -}); -*/ diff --git a/packages/backend/test/extract-mentions.ts b/packages/backend/test/extract-mentions.ts deleted file mode 100644 index 85afb098d8..0000000000 --- a/packages/backend/test/extract-mentions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as assert from 'assert'; - -import { parse } from 'mfm-js'; -import { extractMentions } from '../src/misc/extract-mentions.js'; - -describe('Extract mentions', () => { - it('simple', () => { - const ast = parse('@foo @bar @baz')!; - const mentions = extractMentions(ast); - assert.deepStrictEqual(mentions, [{ - username: 'foo', - acct: '@foo', - host: null, - }, { - username: 'bar', - acct: '@bar', - host: null, - }, { - username: 'baz', - acct: '@baz', - host: null, - }]); - }); - - it('nested', () => { - const ast = parse('@foo **@bar** @baz')!; - const mentions = extractMentions(ast); - assert.deepStrictEqual(mentions, [{ - username: 'foo', - acct: '@foo', - host: null, - }, { - username: 'bar', - acct: '@bar', - host: null, - }, { - username: 'baz', - acct: '@baz', - host: null, - }]); - }); -}); diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.ts deleted file mode 100644 index ddb0e94b86..0000000000 --- a/packages/backend/test/fetch-resource.ts +++ /dev/null @@ -1,205 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as openapi from '@redocly/openapi-core'; -import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; - -// Request Accept -const ONLY_AP = 'application/activity+json'; -const PREFER_AP = 'application/activity+json, */*'; -const PREFER_HTML = 'text/html, */*'; -const UNSPECIFIED = '*/*'; - -// Response Contet-Type -const AP = 'application/activity+json; charset=utf-8'; -const JSON = 'application/json; charset=utf-8'; -const HTML = 'text/html; charset=utf-8'; - -describe('Fetch resource', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let alicesPost: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - alicesPost = await post(alice, { - text: 'test', - }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('Common', () => { - it('meta', async(async () => { - const res = await request('/meta', { - }); - - assert.strictEqual(res.status, 200); - })); - - it('GET root', async(async () => { - const res = await simpleGet('/'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it('GET docs', async(async () => { - const res = await simpleGet('/docs/ja-JP/about'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it('GET api-doc', async(async () => { - const res = await simpleGet('/api-doc'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it('GET api.json', async(async () => { - const res = await simpleGet('/api.json'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, JSON); - })); - - it('Validate api.json', async(async () => { - const config = await openapi.loadConfig(); - const result = await openapi.bundle({ - config, - ref: `http://localhost:${port}/api.json`, - }); - - for (const problem of result.problems) { - console.log(`${problem.message} - ${problem.location[0]?.pointer}`); - } - - assert.strictEqual(result.problems.length, 0); - })); - - it('GET favicon.ico', async(async () => { - const res = await simpleGet('/favicon.ico'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/x-icon'); - })); - - it('GET apple-touch-icon.png', async(async () => { - const res = await simpleGet('/apple-touch-icon.png'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/png'); - })); - - it('GET twemoji svg', async(async () => { - const res = await simpleGet('/twemoji/2764.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - })); - - it('GET twemoji svg with hyphen', async(async () => { - const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - })); - }); - - describe('/@:username', () => { - it('Only AP => AP', async(async () => { - const res = await simpleGet(`/@${alice.username}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer AP => AP', async(async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer HTML => HTML', async(async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it('Unspecified => HTML', async(async () => { - const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - }); - - describe('/users/:id', () => { - it('Only AP => AP', async(async () => { - const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer AP => AP', async(async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer HTML => Redirect to /@:username', async(async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - })); - - it('Undecided => HTML', async(async () => { - const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - })); - }); - - describe('/notes/:id', () => { - it('Only AP => AP', async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer AP => AP', async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it('Prefer HTML => HTML', async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it('Unspecified => HTML', async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - }); - - describe('Feeds', () => { - it('RSS', async(async () => { - const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); - })); - - it('ATOM', async(async () => { - const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); - })); - - it('JSON', async(async () => { - const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/json; charset=utf-8'); - })); - }); -}); diff --git a/packages/backend/test/ff-visibility.ts b/packages/backend/test/ff-visibility.ts deleted file mode 100644 index 4f6847be6d..0000000000 --- a/packages/backend/test/ff-visibility.ts +++ /dev/null @@ -1,167 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.js'; - -describe('FF visibility', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'public', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); - - it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - await request('/following/create', { - userId: alice.id, - }, bob); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async(async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); - - describe('AP', () => { - it('ffVisibility が public 以外ならばAPからは取得できない', async(async () => { - { - await request('/i/update', { - ffVisibility: 'public', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); - } - { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - })); - }); -}); diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts deleted file mode 100644 index 09378fec88..0000000000 --- a/packages/backend/test/get-file-info.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as assert from 'assert'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import { getFileInfo } from '../src/misc/get-file-info.js'; -import { async } from './utils.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -describe('Get file info', () => { - it('Empty file', async (async () => { - const path = `${_dirname}/resources/emptyfile`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 0, - md5: 'd41d8cd98f00b204e9800998ecf8427e', - type: { - mime: 'application/octet-stream', - ext: null, - }, - width: undefined, - height: undefined, - orientation: undefined, - }); - })); - - it('Generic JPEG', async (async () => { - const path = `${_dirname}/resources/Lenna.jpg`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 25360, - md5: '091b3f259662aa31e2ffef4519951168', - type: { - mime: 'image/jpeg', - ext: 'jpg', - }, - width: 512, - height: 512, - orientation: undefined, - }); - })); - - it('Generic APNG', async (async () => { - const path = `${_dirname}/resources/anime.png`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 1868, - md5: '08189c607bea3b952704676bb3c979e0', - type: { - mime: 'image/apng', - ext: 'apng', - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it('Generic AGIF', async (async () => { - const path = `${_dirname}/resources/anime.gif`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 2248, - md5: '32c47a11555675d9267aee1a86571e7e', - type: { - mime: 'image/gif', - ext: 'gif', - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it('PNG with alpha', async (async () => { - const path = `${_dirname}/resources/with-alpha.png`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 3772, - md5: 'f73535c3e1e27508885b69b10cf6e991', - type: { - mime: 'image/png', - ext: 'png', - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it('Generic SVG', async (async () => { - const path = `${_dirname}/resources/image.svg`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 505, - md5: 'b6f52b4b021e7b92cdd04509c7267965', - type: { - mime: 'image/svg+xml', - ext: 'svg', - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it('SVG with XML definition', async (async () => { - // https://github.com/misskey-dev/misskey/issues/4413 - const path = `${_dirname}/resources/with-xml-def.svg`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 544, - md5: '4b7a346cde9ccbeb267e812567e33397', - type: { - mime: 'image/svg+xml', - ext: 'svg', - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it('Dimension limit', async (async () => { - const path = `${_dirname}/resources/25000x25000.png`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 75933, - md5: '268c5dde99e17cf8fe09f1ab3f97df56', - type: { - mime: 'application/octet-stream', // do not treat as image - ext: null, - }, - width: 25000, - height: 25000, - orientation: undefined, - }); - })); - - it('Rotate JPEG', async (async () => { - const path = `${_dirname}/resources/rotate.jpg`; - const info = await getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 12624, - md5: '68d5b2d8d1d1acbbce99203e3ec3857e', - type: { - mime: 'image/jpeg', - ext: 'jpg', - }, - width: 512, - height: 256, - orientation: 8, - }); - })); -}); diff --git a/packages/backend/test/mfm.ts b/packages/backend/test/mfm.ts deleted file mode 100644 index 5218942a5a..0000000000 --- a/packages/backend/test/mfm.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as assert from 'assert'; -import * as mfm from 'mfm-js'; - -import { toHtml } from '../src/mfm/to-html.js'; -import { fromHtml } from '../src/mfm/from-html.js'; - -describe('toHtml', () => { - it('br', () => { - const input = 'foo\nbar\nbaz'; - const output = '

foo
bar
baz

'; - assert.equal(toHtml(mfm.parse(input)), output); - }); - - it('br alt', () => { - const input = 'foo\r\nbar\rbaz'; - const output = '

foo
bar
baz

'; - assert.equal(toHtml(mfm.parse(input)), output); - }); -}); - -describe('fromHtml', () => { - it('p', () => { - assert.deepStrictEqual(fromHtml('

a

b

'), 'a\n\nb'); - }); - - it('block element', () => { - assert.deepStrictEqual(fromHtml('
a
b
'), 'a\nb'); - }); - - it('inline element', () => { - assert.deepStrictEqual(fromHtml('
  • a
  • b
'), 'a\nb'); - }); - - it('block code', () => { - assert.deepStrictEqual(fromHtml('
a\nb
'), '```\na\nb\n```'); - }); - - it('inline code', () => { - assert.deepStrictEqual(fromHtml('a'), '`a`'); - }); - - it('quote', () => { - assert.deepStrictEqual(fromHtml('
a\nb
'), '> a\n> b'); - }); - - it('br', () => { - assert.deepStrictEqual(fromHtml('

abc

d

'), 'abc\n\nd'); - }); - - it('link with different text', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c](https://example.com/b) d'); - }); - - it('link with different text, but not encoded', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c]() d'); - }); - - it('link with same text', () => { - assert.deepStrictEqual(fromHtml('

a https://example.com/b d

'), 'a https://example.com/b d'); - }); - - it('link with same text, but not encoded', () => { - assert.deepStrictEqual(fromHtml('

a https://example.com/ä d

'), 'a d'); - }); - - it('link with no url', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c](b) d'); - }); - - it('link without href', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a c d'); - }); - - it('link without text', () => { - assert.deepStrictEqual(fromHtml('

a d

'), 'a https://example.com/b d'); - }); - - it('link without both', () => { - assert.deepStrictEqual(fromHtml('

a d

'), 'a d'); - }); - - it('mention', () => { - assert.deepStrictEqual(fromHtml('

a @user d

'), 'a @user@example.com d'); - }); - - it('hashtag', () => { - assert.deepStrictEqual(fromHtml('

a #a d

', ['#a']), 'a #a d'); - }); -}); diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.ts deleted file mode 100644 index 465633973c..0000000000 --- a/packages/backend/test/mute.ts +++ /dev/null @@ -1,123 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.js'; - -describe('Mute', () => { - let p: childProcess.ChildProcess; - - // alice mutes carol - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('ミュート作成', async(async () => { - const res = await request('/mute/create', { - userId: carol.id, - }, alice); - - assert.strictEqual(res.status, 204); - })); - - it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => { - const bobNote = await post(bob, { text: '@alice hi' }); - const carolNote = await post(carol, { text: '@alice hi' }); - - const res = await request('/notes/mentions', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - })); - - it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - await post(carol, { text: '@alice hi' }); - - const res = await request('/i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - })); - - it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); - - assert.strictEqual(fired, false); - }); - - it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - await request('/notifications/mark-all-as-read', {}, alice); - - const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); - - assert.strictEqual(fired, false); - }); - - describe('Timeline', () => { - it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request('/notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - })); - - it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => { - const aliceNote = await post(alice); - const carolNote = await post(carol); - const bobNote = await post(bob, { - renoteId: carolNote.id, - }); - - const res = await request('/notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - })); - }); - - describe('Notification', () => { - it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => { - const aliceNote = await post(alice); - await react(bob, aliceNote, 'like'); - await react(carol, aliceNote, 'like'); - - const res = await request('/i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); - })); - }); -}); diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.ts deleted file mode 100644 index b495d8b7bb..0000000000 --- a/packages/backend/test/note.ts +++ /dev/null @@ -1,370 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Note } from '../src/models/entities/note.js'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.js'; - -describe('Note', () => { - let p: childProcess.ChildProcess; - let Notes: any; - - let alice: any; - let bob: any; - - before(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Notes = connection.getRepository(Note); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('投稿できる', async(async () => { - const post = { - text: 'test', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - })); - - it('ファイルを添付できる', async(async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - - const res = await request('/notes/create', { - fileIds: [file.id], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); - })); - - it('他人のファイルは無視', async(async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - - const res = await request('/notes/create', { - text: 'test', - fileIds: [file.id], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it('存在しないファイルは無視', async(async () => { - const res = await request('/notes/create', { - text: 'test', - fileIds: ['000000000000000000000000'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it('不正なファイルIDは無視', async(async () => { - const res = await request('/notes/create', { - fileIds: ['kyoppie'], - }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it('返信できる', async(async () => { - const bobPost = await post(bob, { - text: 'foo', - }); - - const alicePost = { - text: 'bar', - replyId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); - assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); - })); - - it('renoteできる', async(async () => { - const bobPost = await post(bob, { - text: 'test', - }); - - const alicePost = { - renoteId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - })); - - it('引用renoteできる', async(async () => { - const bobPost = await post(bob, { - text: 'test', - }); - - const alicePost = { - text: 'test', - renoteId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - })); - - it('文字数ぎりぎりで怒られない', async(async () => { - const post = { - text: '!'.repeat(3000), - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 200); - })); - - it('文字数オーバーで怒られる', async(async () => { - const post = { - text: '!'.repeat(3001), - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - })); - - it('存在しないリプライ先で怒られる', async(async () => { - const post = { - text: 'test', - replyId: '000000000000000000000000', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - })); - - it('存在しないrenote対象で怒られる', async(async () => { - const post = { - renoteId: '000000000000000000000000', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - })); - - it('不正なリプライ先IDで怒られる', async(async () => { - const post = { - text: 'test', - replyId: 'foo', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - })); - - it('不正なrenote対象IDで怒られる', async(async () => { - const post = { - renoteId: 'foo', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーにメンションできる', async(async () => { - const post = { - text: '@ghost yo', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - })); - - it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => { - const post = { - text: '@bob @bob @bob yo', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - - const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); - assert.deepStrictEqual(noteDoc.mentions, [bob.id]); - })); - - describe('notes/create', () => { - it('投票を添付できる', async(async () => { - const res = await request('/notes/create', { - text: 'test', - poll: { - choices: ['foo', 'bar'], - }, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.poll != null, true); - })); - - it('投票の選択肢が無くて怒られる', async(async () => { - const res = await request('/notes/create', { - poll: {}, - }, alice); - assert.strictEqual(res.status, 400); - })); - - it('投票の選択肢が無くて怒られる (空の配列)', async(async () => { - const res = await request('/notes/create', { - poll: { - choices: [], - }, - }, alice); - assert.strictEqual(res.status, 400); - })); - - it('投票の選択肢が1つで怒られる', async(async () => { - const res = await request('/notes/create', { - poll: { - choices: ['Strawberry Pasta'], - }, - }, alice); - assert.strictEqual(res.status, 400); - })); - - it('投票できる', async(async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - assert.strictEqual(res.status, 204); - })); - - it('複数投票できない', async(async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2, - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('許可されている場合は複数投票できる', async(async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - multiple: true, - }, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2, - }, alice); - - assert.strictEqual(res.status, 204); - })); - - it('締め切られている場合は投票できない', async(async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - expiredAfter: 1, - }, - }, alice); - - await new Promise(x => setTimeout(x, 2)); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/delete', () => { - it('delete a reply', async(async () => { - const mainNoteRes = await api('notes/create', { - text: 'main post', - }, alice); - const replyOneRes = await api('notes/create', { - text: 'reply one', - replyId: mainNoteRes.body.createdNote.id, - }, alice); - const replyTwoRes = await api('notes/create', { - text: 'reply two', - replyId: mainNoteRes.body.createdNote.id, - }, alice); - - const deleteOneRes = await api('notes/delete', { - noteId: replyOneRes.body.createdNote.id, - }, alice); - - assert.strictEqual(deleteOneRes.status, 204); - let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.strictEqual(mainNote.repliesCount, 1); - - const deleteTwoRes = await api('notes/delete', { - noteId: replyTwoRes.body.createdNote.id, - }, alice); - - assert.strictEqual(deleteTwoRes.status, 204); - mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.strictEqual(mainNote.repliesCount, 0); - })); - }); -}); diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts index 0f4b00065f..c1ff63eada 100644 --- a/packages/backend/test/prelude/maybe.ts +++ b/packages/backend/test/prelude/maybe.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { just, nothing } from '../../src/prelude/maybe.js'; +import { just, nothing } from '../../src/misc/prelude/maybe.js'; describe('just', () => { it('has a value', () => { diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index df102c8dfe..574f2fffdb 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { query } from '../../src/prelude/url.js'; +import { query } from '../../src/misc/prelude/url.js'; describe('url', () => { it('query', () => { diff --git a/packages/backend/test/reaction-lib.ts b/packages/backend/test/reaction-lib.ts deleted file mode 100644 index 7c61dc76c2..0000000000 --- a/packages/backend/test/reaction-lib.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* -import * as assert from 'assert'; - -import { toDbReaction } from '../src/misc/reaction-lib.js'; - -describe('toDbReaction', async () => { - it('既存の文字列リアクションはそのまま', async () => { - assert.strictEqual(await toDbReaction('like'), 'like'); - }); - - it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { - assert.strictEqual(await toDbReaction('🍮'), '🍮'); - }); - - it('プリン以外の既存のリアクションは文字列化する like', async () => { - assert.strictEqual(await toDbReaction('👍'), 'like'); - }); - - it('プリン以外の既存のリアクションは文字列化する love', async () => { - assert.strictEqual(await toDbReaction('❤️'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { - assert.strictEqual(await toDbReaction('❤'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する laugh', async () => { - assert.strictEqual(await toDbReaction('😆'), 'laugh'); - }); - - it('プリン以外の既存のリアクションは文字列化する hmm', async () => { - assert.strictEqual(await toDbReaction('🤔'), 'hmm'); - }); - - it('プリン以外の既存のリアクションは文字列化する surprise', async () => { - assert.strictEqual(await toDbReaction('😮'), 'surprise'); - }); - - it('プリン以外の既存のリアクションは文字列化する congrats', async () => { - assert.strictEqual(await toDbReaction('🎉'), 'congrats'); - }); - - it('プリン以外の既存のリアクションは文字列化する angry', async () => { - assert.strictEqual(await toDbReaction('💢'), 'angry'); - }); - - it('プリン以外の既存のリアクションは文字列化する confused', async () => { - assert.strictEqual(await toDbReaction('😥'), 'confused'); - }); - - it('プリン以外の既存のリアクションは文字列化する rip', async () => { - assert.strictEqual(await toDbReaction('😇'), 'rip'); - }); - - it('それ以外はUnicodeのまま', async () => { - assert.strictEqual(await toDbReaction('🍅'), '🍅'); - }); - - it('異体字セレクタ除去', async () => { - assert.strictEqual(await toDbReaction('㊗️'), '㊗'); - }); - - it('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await toDbReaction('㊗'), '㊗'); - }); - - it('fallback - undefined', async () => { - assert.strictEqual(await toDbReaction(undefined), 'like'); - }); - - it('fallback - null', async () => { - assert.strictEqual(await toDbReaction(null), 'like'); - }); - - it('fallback - empty', async () => { - assert.strictEqual(await toDbReaction(''), 'like'); - }); - - it('fallback - unknown', async () => { - assert.strictEqual(await toDbReaction('unknown'), 'like'); - }); -}); -*/ diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts deleted file mode 100644 index 621d07f9c2..0000000000 --- a/packages/backend/test/streaming.ts +++ /dev/null @@ -1,545 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Following } from '../src/models/entities/following.js'; -import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.js'; - -describe('Streaming', () => { - let p: childProcess.ChildProcess; - let Followings: any; - - const follow = async (follower: any, followee: any) => { - await Followings.save({ - id: 'a', - createdAt: new Date(), - followerId: follower.id, - followeeId: followee.id, - followerHost: follower.host, - followerInbox: null, - followerSharedInbox: null, - followeeHost: followee.host, - followeeInbox: null, - followeeSharedInbox: null, - }); - }; - - describe('Streaming', () => { - // Local users - let ayano: any; - let kyoko: any; - let chitose: any; - - // Remote users - let akari: any; - let chinatsu: any; - - let kyokoNote: any; - let list: any; - - before(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Followings = connection.getRepository(Following); - - ayano = await signup({ username: 'ayano' }); - kyoko = await signup({ username: 'kyoko' }); - chitose = await signup({ username: 'chitose' }); - - akari = await signup({ username: 'akari', host: 'example.com' }); - chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); - - kyokoNote = await post(kyoko, { text: 'foo' }); - - // Follow: ayano => kyoko - await api('following/create', { userId: kyoko.id }, ayano); - - // Follow: ayano => akari - await follow(ayano, akari); - - // List: chitose => ayano, kyoko - list = await api('users/lists/create', { - name: 'my list', - }, chitose).then(x => x.body); - - await api('users/lists/push', { - listId: list.id, - userId: ayano.id, - }, chitose); - - await api('users/lists/push', { - listId: list.id, - userId: kyoko.id, - }, chitose); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('Events', () => { - it('mention event', async () => { - const fired = await waitFire( - kyoko, 'main', // kyoko:main - () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko - msg => msg.type === 'mention' && msg.body.userId === ayano.id // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - it('renote event', async () => { - const fired = await waitFire( - kyoko, 'main', // kyoko:main - () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote - msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id // wait renote - ); - - assert.strictEqual(fired, true); - }); - }); - - describe('Home Timeline', () => { - it('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:Home - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo' - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしているユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないユーザーの投稿は流れない', async () => { - const fired = await waitFire( - kyoko, 'homeTimeline', // kyoko:home - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.userId === ayano.id // wait ayano - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしているユーザーのダイレクト投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id], }, kyoko), // kyoko dm => ayano - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id], }, kyoko), // kyoko dm => chitose - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); // Home - - describe('Local Timeline', () => { - it('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo' - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないローカルユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it('リモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, akari), // akari posts - msg => msg.type === 'note' && msg.body.userId === akari.id // wait akari - ); - - assert.strictEqual(fired, false); - }); - - it('ホーム指定の投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Hybrid Timeline', () => { - it('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo' - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないローカルユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしているリモートユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, akari), // akari posts - msg => msg.type === 'note' && msg.body.userId === akari.id // wait akari - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないリモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしているユーザーのダイレクト投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしているユーザーのホーム投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id - ); - - assert.strictEqual(fired, false); - }); - - it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Global Timeline', () => { - it('フォローしていないローカルユーザーの投稿が流れる', () => async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it('フォローしていないリモートユーザーの投稿が流れる', () => async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu - ); - - assert.strictEqual(fired, true); - }); - - it('ホーム投稿は流れない', () => async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('UserList Timeline', () => { - it('リストに入れているユーザーの投稿が流れる', () => async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo' }, ayano), - msg => msg.type === 'note' && msg.body.userId === ayano.id, - { listId: list.id, } - ); - - assert.strictEqual(fired, true); - }); - - it('リストに入れていないユーザーの投稿は流れない', () => async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo' }, chinatsu), - msg => msg.type === 'note' && msg.body.userId === chinatsu.id, - { listId: list.id, } - ); - - assert.strictEqual(fired, false); - }); - - // #4471 - it('リストに入れているユーザーのダイレクト投稿が流れる', () => async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano), - msg => msg.type === 'note' && msg.body.userId === ayano.id, - { listId: list.id, } - ); - - assert.strictEqual(fired, true); - }); - - // #4335 - it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id, } - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Hashtag Timeline', () => { - it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type == 'note') { - assert.deepStrictEqual(body.text, '#foo'); - ws.close(); - done(); - } - }, { - q: [ - ['foo'], - ], - }); - - post(chitose, { - text: '#foo', - }); - })); - - it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type == 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - } - }, { - q: [ - ['foo', 'bar'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - ws.close(); - done(); - }, 3000); - })); - - it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type == 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - } - }, { - q: [ - ['foo'], - ['bar'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - post(chitose, { - text: '#piyo', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 1); - assert.strictEqual(barCount, 1); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 0); - ws.close(); - done(); - }, 3000); - })); - - it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - let waaaCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type == 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - if (body.text === '#waaa') waaaCount++; - } - }, { - q: [ - ['foo', 'bar'], - ['piyo'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - post(chitose, { - text: '#piyo', - }); - - post(chitose, { - text: '#waaa', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 1); - assert.strictEqual(waaaCount, 0); - ws.close(); - done(); - }, 3000); - })); - }); - }); -}); diff --git a/packages/backend/test/tests/activitypub.ts b/packages/backend/test/tests/activitypub.ts new file mode 100644 index 0000000000..6f549ca9c2 --- /dev/null +++ b/packages/backend/test/tests/activitypub.ts @@ -0,0 +1,89 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import rndstr from 'rndstr'; + +describe('ActivityPub', () => { + describe('Parse minimum object', () => { + const host = 'https://host1.test'; + const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; + const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; + + const actor = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actorId, + type: 'Person', + preferredUsername, + inbox: `${actorId}/inbox`, + outbox: `${actorId}/outbox`, + }; + + const post = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${host}/users/${rndstr('0-9a-z', 8)}`, + type: 'Note', + attributedTo: actor.id, + to: 'https://www.w3.org/ns/activitystreams#Public', + content: 'あ', + }; + + it('Minimum Actor', async () => { + const { MockResolver } = await import('../misc/mock-resolver.js'); + const { createPerson } = await import('../../src/remote/activitypub/models/person.js'); + + const resolver = new MockResolver(); + resolver._register(actor.id, actor); + + const user = await createPerson(actor.id, resolver); + + assert.deepStrictEqual(user.uri, actor.id); + assert.deepStrictEqual(user.username, actor.preferredUsername); + assert.deepStrictEqual(user.inbox, actor.inbox); + }); + + it('Minimum Note', async () => { + const { MockResolver } = await import('../misc/mock-resolver.js'); + const { createNote } = await import('../../src/remote/activitypub/models/note.js'); + + const resolver = new MockResolver(); + resolver._register(actor.id, actor); + resolver._register(post.id, post); + + const note = await createNote(post.id, resolver, true); + + assert.deepStrictEqual(note?.uri, post.id); + assert.deepStrictEqual(note.visibility, 'public'); + assert.deepStrictEqual(note.text, post.content); + }); + }); + + describe('Truncate long name', () => { + const host = 'https://host1.test'; + const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; + const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; + + const name = rndstr('0-9a-z', 129); + + const actor = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actorId, + type: 'Person', + preferredUsername, + name, + inbox: `${actorId}/inbox`, + outbox: `${actorId}/outbox`, + }; + + it('Actor', async () => { + const { MockResolver } = await import('../misc/mock-resolver.js'); + const { createPerson } = await import('../../src/remote/activitypub/models/person.js'); + + const resolver = new MockResolver(); + resolver._register(actor.id, actor); + + const user = await createPerson(actor.id, resolver); + + assert.deepStrictEqual(user.name, actor.name.substr(0, 128)); + }); + }); +}); diff --git a/packages/backend/test/tests/ap-request.ts b/packages/backend/test/tests/ap-request.ts new file mode 100644 index 0000000000..2499b86e83 --- /dev/null +++ b/packages/backend/test/tests/ap-request.ts @@ -0,0 +1,55 @@ +import * as assert from 'assert'; +import httpSignature from 'http-signature'; +import { genRsaKeyPair } from '../../src/misc/gen-key-pair.js'; +import { createSignedPost, createSignedGet } from '../../src/remote/activitypub/ap-request.js'; + +export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { + return { + scheme: 'Signature', + params: { + keyId: 'KeyID', // dummy, not used for verify + algorithm: algorithm, + headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify + signature: signature, + }, + signingString: signingString, + algorithm: algorithm.toUpperCase(), + keyId: 'KeyID', // dummy, not used for verify + }; +}; + +describe('ap-request', () => { + it('createSignedPost with verify', async () => { + const keypair = await genRsaKeyPair(); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/inbox'; + const activity = { a: 1 }; + const body = JSON.stringify(activity); + const headers = { + 'User-Agent': 'UA', + }; + + const req = createSignedPost({ key, url, body, additionalHeaders: headers }); + + const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); + + const result = httpSignature.verifySignature(parsed, keypair.publicKey); + assert.deepStrictEqual(result, true); + }); + + it('createSignedGet with verify', async () => { + const keypair = await genRsaKeyPair(); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/outbox'; + const headers = { + 'User-Agent': 'UA', + }; + + const req = createSignedGet({ key, url, additionalHeaders: headers }); + + const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); + + const result = httpSignature.verifySignature(parsed, keypair.publicKey); + assert.deepStrictEqual(result, true); + }); +}); diff --git a/packages/backend/test/tests/extract-mentions.ts b/packages/backend/test/tests/extract-mentions.ts new file mode 100644 index 0000000000..4f9cb68763 --- /dev/null +++ b/packages/backend/test/tests/extract-mentions.ts @@ -0,0 +1,42 @@ +import * as assert from 'assert'; + +import { parse } from 'mfm-js'; +import { extractMentions } from '../../src/misc/extract-mentions.js'; + +describe('Extract mentions', () => { + it('simple', () => { + const ast = parse('@foo @bar @baz')!; + const mentions = extractMentions(ast); + assert.deepStrictEqual(mentions, [{ + username: 'foo', + acct: '@foo', + host: null, + }, { + username: 'bar', + acct: '@bar', + host: null, + }, { + username: 'baz', + acct: '@baz', + host: null, + }]); + }); + + it('nested', () => { + const ast = parse('@foo **@bar** @baz')!; + const mentions = extractMentions(ast); + assert.deepStrictEqual(mentions, [{ + username: 'foo', + acct: '@foo', + host: null, + }, { + username: 'bar', + acct: '@bar', + host: null, + }, { + username: 'baz', + acct: '@baz', + host: null, + }]); + }); +}); diff --git a/packages/backend/test/tests/mfm.ts b/packages/backend/test/tests/mfm.ts new file mode 100644 index 0000000000..5087e84a1a --- /dev/null +++ b/packages/backend/test/tests/mfm.ts @@ -0,0 +1,89 @@ +import * as assert from 'assert'; +import * as mfm from 'mfm-js'; + +import { toHtml } from '../../src/mfm/to-html.js'; +import { fromHtml } from '../../src/mfm/from-html.js'; + +describe('toHtml', () => { + it('br', () => { + const input = 'foo\nbar\nbaz'; + const output = '

foo
bar
baz

'; + assert.equal(toHtml(mfm.parse(input)), output); + }); + + it('br alt', () => { + const input = 'foo\r\nbar\rbaz'; + const output = '

foo
bar
baz

'; + assert.equal(toHtml(mfm.parse(input)), output); + }); +}); + +describe('fromHtml', () => { + it('p', () => { + assert.deepStrictEqual(fromHtml('

a

b

'), 'a\n\nb'); + }); + + it('block element', () => { + assert.deepStrictEqual(fromHtml('
a
b
'), 'a\nb'); + }); + + it('inline element', () => { + assert.deepStrictEqual(fromHtml('
  • a
  • b
'), 'a\nb'); + }); + + it('block code', () => { + assert.deepStrictEqual(fromHtml('
a\nb
'), '```\na\nb\n```'); + }); + + it('inline code', () => { + assert.deepStrictEqual(fromHtml('a'), '`a`'); + }); + + it('quote', () => { + assert.deepStrictEqual(fromHtml('
a\nb
'), '> a\n> b'); + }); + + it('br', () => { + assert.deepStrictEqual(fromHtml('

abc

d

'), 'abc\n\nd'); + }); + + it('link with different text', () => { + assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c](https://example.com/b) d'); + }); + + it('link with different text, but not encoded', () => { + assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c]() d'); + }); + + it('link with same text', () => { + assert.deepStrictEqual(fromHtml('

a https://example.com/b d

'), 'a https://example.com/b d'); + }); + + it('link with same text, but not encoded', () => { + assert.deepStrictEqual(fromHtml('

a https://example.com/ä d

'), 'a d'); + }); + + it('link with no url', () => { + assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c](b) d'); + }); + + it('link without href', () => { + assert.deepStrictEqual(fromHtml('

a c d

'), 'a c d'); + }); + + it('link without text', () => { + assert.deepStrictEqual(fromHtml('

a d

'), 'a https://example.com/b d'); + }); + + it('link without both', () => { + assert.deepStrictEqual(fromHtml('

a d

'), 'a d'); + }); + + it('mention', () => { + assert.deepStrictEqual(fromHtml('

a @user d

'), 'a @user@example.com d'); + }); + + it('hashtag', () => { + assert.deepStrictEqual(fromHtml('

a #a d

', ['#a']), 'a #a d'); + }); +}); diff --git a/packages/backend/test/tests/reaction-lib.ts b/packages/backend/test/tests/reaction-lib.ts new file mode 100644 index 0000000000..7c61dc76c2 --- /dev/null +++ b/packages/backend/test/tests/reaction-lib.ts @@ -0,0 +1,83 @@ +/* +import * as assert from 'assert'; + +import { toDbReaction } from '../src/misc/reaction-lib.js'; + +describe('toDbReaction', async () => { + it('既存の文字列リアクションはそのまま', async () => { + assert.strictEqual(await toDbReaction('like'), 'like'); + }); + + it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { + assert.strictEqual(await toDbReaction('🍮'), '🍮'); + }); + + it('プリン以外の既存のリアクションは文字列化する like', async () => { + assert.strictEqual(await toDbReaction('👍'), 'like'); + }); + + it('プリン以外の既存のリアクションは文字列化する love', async () => { + assert.strictEqual(await toDbReaction('❤️'), 'love'); + }); + + it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { + assert.strictEqual(await toDbReaction('❤'), 'love'); + }); + + it('プリン以外の既存のリアクションは文字列化する laugh', async () => { + assert.strictEqual(await toDbReaction('😆'), 'laugh'); + }); + + it('プリン以外の既存のリアクションは文字列化する hmm', async () => { + assert.strictEqual(await toDbReaction('🤔'), 'hmm'); + }); + + it('プリン以外の既存のリアクションは文字列化する surprise', async () => { + assert.strictEqual(await toDbReaction('😮'), 'surprise'); + }); + + it('プリン以外の既存のリアクションは文字列化する congrats', async () => { + assert.strictEqual(await toDbReaction('🎉'), 'congrats'); + }); + + it('プリン以外の既存のリアクションは文字列化する angry', async () => { + assert.strictEqual(await toDbReaction('💢'), 'angry'); + }); + + it('プリン以外の既存のリアクションは文字列化する confused', async () => { + assert.strictEqual(await toDbReaction('😥'), 'confused'); + }); + + it('プリン以外の既存のリアクションは文字列化する rip', async () => { + assert.strictEqual(await toDbReaction('😇'), 'rip'); + }); + + it('それ以外はUnicodeのまま', async () => { + assert.strictEqual(await toDbReaction('🍅'), '🍅'); + }); + + it('異体字セレクタ除去', async () => { + assert.strictEqual(await toDbReaction('㊗️'), '㊗'); + }); + + it('異体字セレクタ除去 必要なし', async () => { + assert.strictEqual(await toDbReaction('㊗'), '㊗'); + }); + + it('fallback - undefined', async () => { + assert.strictEqual(await toDbReaction(undefined), 'like'); + }); + + it('fallback - null', async () => { + assert.strictEqual(await toDbReaction(null), 'like'); + }); + + it('fallback - empty', async () => { + assert.strictEqual(await toDbReaction(''), 'like'); + }); + + it('fallback - unknown', async () => { + assert.strictEqual(await toDbReaction('unknown'), 'like'); + }); +}); +*/ diff --git a/packages/backend/test/thread-mute.ts b/packages/backend/test/thread-mute.ts deleted file mode 100644 index cd3e519394..0000000000 --- a/packages/backend/test/thread-mute.ts +++ /dev/null @@ -1,103 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.js'; - -describe('Note thread mute', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => { - const bobNote = await post(bob, { text: '@alice @carol root note' }); - const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - - const res = await request('/notes/mentions', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); - })); - - it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - const res = await request('/i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - })); - - it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - let fired = false; - - const ws = await connectStream(alice, 'main', async ({ type, body }) => { - if (type === 'unreadMention') { - if (body === bobNote.id) return; - fired = true; - } - }); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - setTimeout(() => { - assert.strictEqual(fired, false); - ws.close(); - done(); - }, 5000); - })); - - it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => { - const bobNote = await post(bob, { text: '@alice @carol root note' }); - const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - - const res = await request('/i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); - - // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい - })); -}); diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index bc7a9968b5..5d91d0923a 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -32,7 +32,8 @@ ], "lib": [ "esnext" - ] + ], + "types": ["jest"] }, "compileOnSave": false, "include": [ diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts new file mode 100644 index 0000000000..b876deb545 --- /dev/null +++ b/packages/backend/test/unit/FileInfoService.ts @@ -0,0 +1,237 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { ModuleMocker } from 'jest-mock'; +import { Test } from '@nestjs/testing'; +import { GlobalModule } from '@/GlobalModule.js'; +import { FileInfoService } from '@/core/FileInfoService.js'; +import { DI } from '@/di-symbols.js'; +import { AiService } from '@/core/AiService.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { jest } from '@jest/globals'; +import type { MockFunctionMetadata } from 'jest-mock'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); +const resources = `${_dirname}/../resources`; + +const moduleMocker = new ModuleMocker(global); + +describe('FileInfoService', () => { + let app: TestingModule; + let fileInfoService: FileInfoService; + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + AiService, + FileInfoService, + ], + }) + .useMocker((token) => { + //if (token === AiService) { + // return { }; + //} + if (typeof token === 'function') { + const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; + const Mock = moduleMocker.generateFromMetadata(mockMetadata); + return new Mock(); + } + }) + .compile(); + + app.enableShutdownHooks(); + + fileInfoService = app.get(FileInfoService); + }); + + afterAll(async () => { + await app.close(); + }); + + it('Empty file', async () => { + const path = `${resources}/emptyfile`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 0, + md5: 'd41d8cd98f00b204e9800998ecf8427e', + type: { + mime: 'application/octet-stream', + ext: null, + }, + width: undefined, + height: undefined, + orientation: undefined, + }); + }); + + it('Generic JPEG', async () => { + const path = `${resources}/Lenna.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', + type: { + mime: 'image/jpeg', + ext: 'jpg', + }, + width: 512, + height: 512, + orientation: undefined, + }); + }); + + it('Generic APNG', async () => { + const path = `${resources}/anime.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 1868, + md5: '08189c607bea3b952704676bb3c979e0', + type: { + mime: 'image/apng', + ext: 'apng', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + it('Generic AGIF', async () => { + const path = `${resources}/anime.gif`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 2248, + md5: '32c47a11555675d9267aee1a86571e7e', + type: { + mime: 'image/gif', + ext: 'gif', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + it('PNG with alpha', async () => { + const path = `${resources}/with-alpha.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 3772, + md5: 'f73535c3e1e27508885b69b10cf6e991', + type: { + mime: 'image/png', + ext: 'png', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + it('Generic SVG', async () => { + const path = `${resources}/image.svg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 505, + md5: 'b6f52b4b021e7b92cdd04509c7267965', + type: { + mime: 'image/svg+xml', + ext: 'svg', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + it('SVG with XML definition', async () => { + // https://github.com/misskey-dev/misskey/issues/4413 + const path = `${resources}/with-xml-def.svg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 544, + md5: '4b7a346cde9ccbeb267e812567e33397', + type: { + mime: 'image/svg+xml', + ext: 'svg', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + it('Dimension limit', async () => { + const path = `${resources}/25000x25000.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 75933, + md5: '268c5dde99e17cf8fe09f1ab3f97df56', + type: { + mime: 'application/octet-stream', // do not treat as image + ext: null, + }, + width: 25000, + height: 25000, + orientation: undefined, + }); + }); + + it('Rotate JPEG', async () => { + const path = `${resources}/rotate.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 12624, + md5: '68d5b2d8d1d1acbbce99203e3ec3857e', + type: { + mime: 'image/jpeg', + ext: 'jpg', + }, + width: 512, + height: 256, + orientation: 8, + }); + }); +}); diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts new file mode 100644 index 0000000000..bb555648e9 --- /dev/null +++ b/packages/backend/test/unit/RelayService.ts @@ -0,0 +1,96 @@ +process.env.NODE_ENV = 'test'; + +import { jest } from '@jest/globals'; +import { ModuleMocker } from 'jest-mock'; +import { Test } from '@nestjs/testing'; +import { GlobalModule } from '@/GlobalModule.js'; +import { RelayService } from '@/core/RelayService.js'; +import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; +import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import type { RelaysRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { MockFunctionMetadata } from 'jest-mock'; + +const moduleMocker = new ModuleMocker(global); + +describe('RelayService', () => { + let app: TestingModule; + let relayService: RelayService; + let queueService: jest.Mocked; + let relaysRepository: RelaysRepository; + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + IdService, + CreateSystemUserService, + ApRendererService, + RelayService, + ], + }) + .useMocker((token) => { + if (token === QueueService) { + return { deliver: jest.fn() }; + } + if (typeof token === 'function') { + const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; + const Mock = moduleMocker.generateFromMetadata(mockMetadata); + return new Mock(); + } + }) + .compile(); + + app.enableShutdownHooks(); + + relayService = app.get(RelayService); + queueService = app.get(QueueService) as jest.Mocked; + relaysRepository = app.get(DI.relaysRepository); + }); + + afterAll(async () => { + await app.close(); + }); + + it('addRelay', async () => { + const result = await relayService.addRelay('https://example.com'); + + expect(result.inbox).toBe('https://example.com'); + expect(result.status).toBe('requesting'); + expect(queueService.deliver).toHaveBeenCalled(); + expect(queueService.deliver.mock.lastCall![1].type).toBe('Follow'); + expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); + //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); + }); + + it('listRelay', async () => { + const result = await relayService.listRelay(); + + expect(result.length).toBe(1); + expect(result[0].inbox).toBe('https://example.com'); + expect(result[0].status).toBe('requesting'); + }); + + it('removeRelay: succ', async () => { + await relayService.removeRelay('https://example.com'); + + expect(queueService.deliver).toHaveBeenCalled(); + expect(queueService.deliver.mock.lastCall![1].type).toBe('Undo'); + expect(queueService.deliver.mock.lastCall![1].object.type).toBe('Follow'); + expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); + //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); + + const list = await relayService.listRelay(); + expect(list.length).toBe(0); + }); + + it('removeRelay: fail', async () => { + await expect(relayService.removeRelay('https://x.example.com')) + .rejects.toThrow('relay not found'); + }); +}); diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts new file mode 100644 index 0000000000..f1159976fd --- /dev/null +++ b/packages/backend/test/unit/chart.ts @@ -0,0 +1,574 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { jest } from '@jest/globals'; +import * as lolex from '@sinonjs/fake-timers'; +import { DataSource } from 'typeorm'; +import TestChart from '@/core/chart/charts/test.js'; +import TestGroupedChart from '@/core/chart/charts/test-grouped.js'; +import TestUniqueChart from '@/core/chart/charts/test-unique.js'; +import TestIntersectionChart from '@/core/chart/charts/test-intersection.js'; +import { entity as TestChartEntity } from '@/core/chart/charts/entities/test.js'; +import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/test-grouped.js'; +import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js'; +import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js'; +import { loadConfig } from '@/config.js'; +import type { AppLockService } from '@/core/AppLockService'; + +describe('Chart', () => { + const config = loadConfig(); + const appLockService = { + getChartInsertLock: jest.fn().mockImplementation(() => Promise.resolve(() => {})), + } as unknown as jest.Mocked; + + let db: DataSource | undefined; + + let testChart: TestChart; + let testGroupedChart: TestGroupedChart; + let testUniqueChart: TestUniqueChart; + let testIntersectionChart: TestIntersectionChart; + let clock: lolex.InstalledClock; + + beforeEach(async () => { + if (db) db.destroy(); + + db = new DataSource({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + extra: { + statement_timeout: 1000 * 10, + ...config.db.extra, + }, + synchronize: true, + dropSchema: true, + maxQueryExecutionTime: 300, + entities: [ + TestChartEntity.hour, TestChartEntity.day, + TestGroupedChartEntity.hour, TestGroupedChartEntity.day, + TestUniqueChartEntity.hour, TestUniqueChartEntity.day, + TestIntersectionChartEntity.hour, TestIntersectionChartEntity.day, + ], + migrations: ['../../migration/*.js'], + }); + + await db.initialize(); + + testChart = new TestChart(db, appLockService); + testGroupedChart = new TestGroupedChart(db, appLockService); + testUniqueChart = new TestUniqueChart(db, appLockService); + testIntersectionChart = new TestIntersectionChart(db, appLockService); + + clock = lolex.install({ + now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), + shouldClearNativeTimers: true, + }); + }); + + afterEach(() => { + clock.uninstall(); + }); + + afterAll(async () => { + if (db) await db.destroy(); + }); + + it('Can updates', async () => { + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + }); + + it('Can updates (dec)', async () => { + await testChart.decrement(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [1, 0, 0], + inc: [0, 0, 0], + total: [-1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [1, 0, 0], + inc: [0, 0, 0], + total: [-1, 0, 0], + }, + }); + }); + + it('Empty chart', async () => { + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0], + }, + }); + }); + + it('Can updates at multiple times at same time', async () => { + await testChart.increment(); + await testChart.increment(); + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0], + }, + }); + }); + + it('複数回saveされてもデータの更新は一度だけ', async () => { + await testChart.increment(); + await testChart.save(); + await testChart.save(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + }); + + it('Can updates at different times', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('01:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 1, 0], + total: [2, 1, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0], + }, + }); + }); + + // 仕様上はこうなってほしいけど、実装は難しそうなのでskip + /* + it('Can updates at different times without save', async () => { + await testChart.increment(); + + clock.tick('01:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 1, 0], + total: [2, 1, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + }); + */ + + it('Can padding', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('02:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 1], + total: [2, 1, 1], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0], + }, + }); + }); + + // 要求された範囲にログがひとつもない場合でもパディングできる + it('Can padding from past range', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('05:00:00'); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [1, 1, 1], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + }); + + // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる + // Issue #3190 + it('Can padding from past range 2', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('05:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [2, 1, 1], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0], + }, + }); + }); + + it('Can specify offset', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('01:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); + const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0], + }, + }); + }); + + it('Can specify offset (floor time)', async () => { + clock.tick('00:30:00'); + + await testChart.increment(); + await testChart.save(); + + clock.tick('01:30:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); + const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0], + }, + }); + }); + + describe('Grouped', () => { + it('Can updates', async () => { + await testGroupedChart.increment('alice'); + await testGroupedChart.save(); + + const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice'); + const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice'); + const bobChartHours = await testGroupedChart.getChart('hour', 3, null, 'bob'); + const bobChartDays = await testGroupedChart.getChart('day', 3, null, 'bob'); + + assert.deepStrictEqual(aliceChartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(aliceChartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(bobChartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0], + }, + }); + + assert.deepStrictEqual(bobChartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0], + }, + }); + }); + }); + + describe('Unique increment', () => { + it('Can updates', async () => { + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('bob'); + await testUniqueChart.save(); + + const chartHours = await testUniqueChart.getChart('hour', 3, null); + const chartDays = await testUniqueChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: [2, 0, 0], + }); + + assert.deepStrictEqual(chartDays, { + foo: [2, 0, 0], + }); + }); + + describe('Intersection', () => { + it('条件が満たされていない場合はカウントされない', async () => { + await testIntersectionChart.addA('alice'); + await testIntersectionChart.addA('bob'); + await testIntersectionChart.addB('carol'); + await testIntersectionChart.save(); + + const chartHours = await testIntersectionChart.getChart('hour', 3, null); + const chartDays = await testIntersectionChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + a: [2, 0, 0], + b: [1, 0, 0], + aAndB: [0, 0, 0], + }); + + assert.deepStrictEqual(chartDays, { + a: [2, 0, 0], + b: [1, 0, 0], + aAndB: [0, 0, 0], + }); + }); + + it('条件が満たされている場合にカウントされる', async () => { + await testIntersectionChart.addA('alice'); + await testIntersectionChart.addA('bob'); + await testIntersectionChart.addB('carol'); + await testIntersectionChart.addB('alice'); + await testIntersectionChart.save(); + + const chartHours = await testIntersectionChart.getChart('hour', 3, null); + const chartDays = await testIntersectionChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + a: [2, 0, 0], + b: [2, 0, 0], + aAndB: [1, 0, 0], + }); + + assert.deepStrictEqual(chartDays, { + a: [2, 0, 0], + b: [2, 0, 0], + aAndB: [1, 0, 0], + }); + }); + }); + }); + + describe('Resync', () => { + it('Can resync', async () => { + testChart.total = 1; + + await testChart.resync(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [1, 0, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [1, 0, 0], + }, + }); + }); + + it('Can resync (2)', async () => { + await testChart.increment(); + await testChart.save(); + + clock.tick('01:00:00'); + + testChart.total = 100; + + await testChart.resync(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 1, 0], + total: [100, 1, 0], + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [100, 0, 0], + }, + }); + }); + }); +}); diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.ts deleted file mode 100644 index 4447754d66..0000000000 --- a/packages/backend/test/user-notes.ts +++ /dev/null @@ -1,61 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.js'; - -describe('users/notes', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let jpgNote: any; - let pngNote: any; - let jpgPngNote: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); - jpgNote = await post(alice, { - fileIds: [jpg.id], - }); - pngNote = await post(alice, { - fileIds: [png.id], - }); - jpgPngNote = await post(alice, { - fileIds: [jpg.id, png.id], - }); - }); - - after(async() => { - await shutdownServer(p); - }); - - it('ファイルタイプ指定 (jpg)', async(async () => { - const res = await request('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 2); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - })); - - it('ファイルタイプ指定 (jpg or png)', async(async () => { - const res = await request('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg', 'image/png'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 3); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - })); -}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 245cf858d8..c8fd41e1d3 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -6,13 +6,13 @@ import * as childProcess from 'child_process'; import * as http from 'node:http'; import { SIGKILL } from 'constants'; import WebSocket from 'ws'; -import * as misskey from 'misskey-js'; import fetch from 'node-fetch'; import FormData from 'form-data'; import { DataSource } from 'typeorm'; +import got, { RequestError } from 'got'; import loadConfig from '../src/config/load.js'; -import { entities } from '../src/db/postgre.js'; -import got from 'got'; +import { entities } from '../src/postgre.js'; +import type * as misskey from 'misskey-js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -20,56 +20,53 @@ const _dirname = dirname(_filename); const config = loadConfig(); export const port = config.port; -export const async = (fn: Function) => (done: Function) => { - fn().then(() => { - done(); - }, (err: Error) => { - done(err); - }); -}; - export const api = async (endpoint: string, params: any, me?: any) => { endpoint = endpoint.replace(/^\//, ''); const auth = me ? { - i: me.token + i: me.token, } : {}; - const res = await got(`http://localhost:${port}/api/${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(Object.assign(auth, params)), - retry: { - limit: 0, - }, - hooks: { - beforeError: [ - error => { - const { response } = error; - if (response && response.body) console.warn(response.body); - return error; - } - ] - }, - }); - - const status = res.statusCode; - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; + try { + const res = await got(`http://localhost:${port}/api/${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(Object.assign(auth, params)), + retry: { + limit: 0, + }, + }); - return { - status, - body - }; + const status = res.statusCode; + const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; + + return { + status, + body, + }; + } catch (err: unknown) { + if (err instanceof RequestError && err.response) { + const status = err.response.statusCode; + const body = await JSON.parse(err.response.body as string); + + return { + status, + body, + }; + } else { + throw err; + } + } }; -export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { +export const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, } : {}; - const res = await fetch(`http://localhost:${port}/api${endpoint}`, { + const res = await fetch(`http://localhost:${port}/${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -78,7 +75,7 @@ export const request = async (endpoint: string, params: any, me?: any): Promise< }); const status = res.status; - const body = res.status !== 204 ? await res.json().catch() : null; + const body = res.status === 200 ? await res.json().catch() : null; return { body, status, @@ -141,19 +138,21 @@ export const uploadFile = async (user: any, _path?: string): Promise => { export const uploadUrl = async (user: any, url: string) => { let file: any; + const marker = Math.random().toString(); const ws = await connectStream(user, 'main', (msg) => { - if (msg.type === 'driveFileCreated') { - file = msg.body; + if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { + file = msg.body.file; } }); await api('drive/files/upload-from-url', { url, + marker, force: true, }, user); - await sleep(5000); + await sleep(7000); ws.close(); return file; @@ -217,7 +216,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond if (timer) clearTimeout(timer); rej(e); } - }) + }); }; export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => { @@ -268,7 +267,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { database: config.db.db, synchronize: true && !justBorrow, dropSchema: true && !justBorrow, - entities: initEntities || entities, + entities: initEntities ?? entities, }); await db.initialize(); @@ -299,7 +298,8 @@ export function startServer(timeout = 60 * 1000): Promise { const t = setTimeout(() => { p.kill(SIGKILL); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 3be2f0d520..9feb24f24f 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -2,16 +2,306 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.18.8": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483" + integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" + integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.13" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.13" + "@babel/types" "^7.18.13" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.13", "@babel/generator@^7.7.2": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" + integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== + dependencies: + "@babel/types" "^7.18.13" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" + integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" + integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" + integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" + integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" + integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" + integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== + "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": version "7.13.9" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" + integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.2": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" + integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.13" + "@babel/types" "^7.18.13" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" + integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@babel/types@^7.6.1", "@babel/types@^7.9.6": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" @@ -21,6 +311,11 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@bull-board/api@4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-4.2.2.tgz#42838f4fda71a3bdca560ea7c6eb80b3d846f446" @@ -135,11 +430,261 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.0.1.tgz#e0e429cfc89900e3a46ce27f493bf488395ade39" + integrity sha512-SxLvSKf9gk4Rvt3p2KRQWVQ3sVj7S37rjlCHwp2+xNcRO/X+Uw0idbkfOtciUpjghHIxyggqcrrKhThQ+vClLQ== + dependencies: + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + slash "^3.0.0" + +"@jest/core@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.0.1.tgz#a49517795f692a510b6fae55a9c09e659826c472" + integrity sha512-EcFrXkYh8I1GYHRH9V4TU7jr4P6ckaPqGo/z4AIJjHDZxicjYgWB6fx1xFb5bhEM87eUjCF4FAY5t+RamLWQmA== + dependencies: + "@jest/console" "^29.0.1" + "@jest/reporters" "^29.0.1" + "@jest/test-result" "^29.0.1" + "@jest/transform" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.0.0" + jest-config "^29.0.1" + jest-haste-map "^29.0.1" + jest-message-util "^29.0.1" + jest-regex-util "^29.0.0" + jest-resolve "^29.0.1" + jest-resolve-dependencies "^29.0.1" + jest-runner "^29.0.1" + jest-runtime "^29.0.1" + jest-snapshot "^29.0.1" + jest-util "^29.0.1" + jest-validate "^29.0.1" + jest-watcher "^29.0.1" + micromatch "^4.0.4" + pretty-format "^29.0.1" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.0.1.tgz#d236ce9e906744ac58bfc59ae6f7c9882ace7927" + integrity sha512-iLcFfoq2K6DAB+Mc+2VNLzZVmHdwQFeSqvoM/X8SMON6s/+yEi1iuRX3snx/JfwSnvmiMXjSr0lktxNxOcqXYA== + dependencies: + "@jest/fake-timers" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + jest-mock "^29.0.1" + +"@jest/expect-utils@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.1.tgz#c1a84ee66caaef537f351dd82f7c63d559cf78d5" + integrity sha512-Tw5kUUOKmXGQDmQ9TSgTraFFS7HMC1HG/B7y0AN2G2UzjdAXz9BzK2rmNpCSDl7g7y0Gf/VLBm//blonvhtOTQ== + dependencies: + jest-get-type "^29.0.0" + +"@jest/expect@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.0.1.tgz#0ffde7f5b4c87f1dd6f8664726bd53f6cd1f7014" + integrity sha512-qKB3q52XDV8VUEiqKKLgLrJx7puQ8sYVqIDlul6n7SIXWS97DOK3KqbR2rDDaMtmenRHqEUl2fI+aFzx0oSemA== + dependencies: + expect "^29.0.1" + jest-snapshot "^29.0.1" + +"@jest/fake-timers@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.0.1.tgz#51ba7a82431db479d4b828576c139c4c0dc5e409" + integrity sha512-XZ+kAhLChVQ+KJNa5034p7O1Mz3vtWrelxDcMoxhZkgqmWDaEQAW9qJeutaeCfPvwaEwKYVyKDYfWpcyT8RiMw== + dependencies: + "@jest/types" "^29.0.1" + "@sinonjs/fake-timers" "^9.1.2" + "@types/node" "*" + jest-message-util "^29.0.1" + jest-mock "^29.0.1" + jest-util "^29.0.1" + +"@jest/globals@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.0.1.tgz#764135ad31408fb632b3126793ab3aaed933095f" + integrity sha512-BtZWrVrKRKNUt7T1H2S8Mz31PN7ItROCmH+V5pn10hJDUfjOCTIUwb0WtLZzm0f1tJ3Uvx+5lVZrF/VTKqNaFg== + dependencies: + "@jest/environment" "^29.0.1" + "@jest/expect" "^29.0.1" + "@jest/types" "^29.0.1" + jest-mock "^29.0.1" + +"@jest/reporters@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.0.1.tgz#82a491657031c1cc278bf659905e5094973309ad" + integrity sha512-dM3L8JmYYOsdeXUUVZClQy67Tz/v1sMo9h4AQv2U+716VLHV0zdA6Hh4FQNAHMhYw/95dbZbPX8Q+TRR7Rw+wA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.0.1" + "@jest/test-result" "^29.0.1" + "@jest/transform" "^29.0.1" + "@jest/types" "^29.0.1" + "@jridgewell/trace-mapping" "^0.3.15" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + jest-worker "^29.0.1" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + terminal-link "^2.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/source-map@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.0.0.tgz#f8d1518298089f8ae624e442bbb6eb870ee7783c" + integrity sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.15" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.0.1.tgz#97ac334e4c6f7d016c341cdd500aa423a38e4cdd" + integrity sha512-XCA4whh/igxjBaR/Hg8qwFd/uTsauoD7QAdAYUjV2CSGx0+iunhjoCRRWTwqjQrETRqOJABx6kNfw0+C0vMSgQ== + dependencies: + "@jest/console" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.0.1.tgz#7074b5f89ce30941b5b0fb493a19308d441a30b8" + integrity sha512-3GhSBMCRcWXGluP2Dw7CLP6mNke/t+EcftF5YjzhX1BJmqcatMbtZVwjuCfZy0TCME1GevXy3qTyV5PLpwIFKQ== + dependencies: + "@jest/test-result" "^29.0.1" + graceful-fs "^4.2.9" + jest-haste-map "^29.0.1" + slash "^3.0.0" + +"@jest/transform@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.0.1.tgz#fdaa5d9e135c9bd7addbe65bedd1f15ad028cc7e" + integrity sha512-6UxXtqrPScFdDhoip8ys60dQAIYppQinyR87n9nlasR/ZnFfJohKToqzM29KK4gb9gHRv5oDFChdqZKE0SIhsg== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.0.1" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.0.1" + jest-regex-util "^29.0.0" + jest-util "^29.0.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.1" + +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== + dependencies: + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.1.tgz#1985650acf137bdb81710ff39a4689ec071dd86a" + integrity sha512-ft01rxzVsbh9qZPJ6EFgAIj3PT9FCRfBF9Xljo2/33VDOUjLZr0ZJ2oKANqh9S/K0/GERCsHDAQlBwj7RxA+9g== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.13" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" @@ -153,6 +698,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@koa/cors@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.1.0.tgz#618bb073438cfdbd3ebd0e648a76e33b84f3a3b2" @@ -191,6 +744,35 @@ semver "^7.3.5" tar "^6.1.11" +"@nestjs/common@9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.0.11.tgz#5747cfbb5d94d909bc2894bf2a5bec83fca809c5" + integrity sha512-oYLIcOal3QOwcqt6goXovRNg8ZkalyOMjH0oYYzfJLrait6P7c6nAeWHu4qFDThY7GoZHEanLgji1qlqVEW09g== + dependencies: + iterare "1.2.1" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/core@9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.0.11.tgz#1bed969d81685f17d4a01f854c43a222dd3e13ce" + integrity sha512-DYyoiWSGebDAG8WSfG/ue88HBU39kAJTi2YXftWdVSl1LFveV+pwKY83P2qX0ND38TS8WktFYpaMkXslf97BBQ== + dependencies: + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + object-hash "3.0.0" + path-to-regexp "3.2.0" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/testing@9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-9.0.11.tgz#8e60107a7aaf9fc7b2bf29d473c2bdb1887de017" + integrity sha512-tT+yj3av7ZJb9Cy09C4+FoUULvzUntf81g5eK5shRVeQ35RWqr7E5Uq77B7ePUF2Er/TictVZk43d7rKq1ClNA== + dependencies: + tslib "2.4.0" + "@node-redis/bloom@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@node-redis/bloom/-/bloom-1.0.1.tgz#144474a0b7dc4a4b91badea2cfa9538ce0a1854e" @@ -278,6 +860,15 @@ pngjs-nozlib "1.0.0" through "2.3.4" +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + "@peertube/http-signature@1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@peertube/http-signature/-/http-signature-1.7.0.tgz#12a84f3fc62e786aa3a2eb09426417bad65736dc" @@ -313,6 +904,11 @@ pluralize "^8.0.0" yaml-ast-parser "0.0.43" +"@sinclair/typebox@^0.24.1": + version "0.24.31" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.31.tgz#3f3752bc830a9daa4a0185573f0bf870089c3222" + integrity sha512-uWZaAsh9WFhcY1rWLLcMU/omiIIAQ/PmgqplaF6UWY6ULPH0ZO8hupJRAydzlTQZJIK3Voz8o8dYlEx+Cm6BAA== + "@sindresorhus/is@^4.0.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" @@ -330,7 +926,7 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@9.1.2": +"@sinonjs/fake-timers@9.1.2", "@sinonjs/fake-timers@^9.1.2": version "9.1.2" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== @@ -488,6 +1084,39 @@ dependencies: "@types/node" "*" +"@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.18.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.1.tgz#ce5e2c8c272b99b7a9fd69fa39f0b4cd85028bd9" + integrity sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA== + dependencies: + "@babel/types" "^7.3.0" + "@types/bcryptjs@2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" @@ -598,6 +1227,13 @@ dependencies: "@types/node" "*" +"@types/graceful-fs@^4.1.3": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + "@types/http-assert@*": version "1.5.1" resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" @@ -620,6 +1256,33 @@ dependencies: "@types/node" "*" +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.0.0.tgz#bc66835bf6b09d6a47e22c21d7f5b82692e60e72" + integrity sha512-X6Zjz3WO4cT39Gkl0lZ2baFRaEMqJl5NC1OjElkwtNzAlbkr2K/WJXkBkH5VP0zx4Hgsd2TZYdOEfvp2Dxia+Q== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/js-yaml@4.0.5": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" @@ -786,11 +1449,6 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/mocha@9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" - integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== - "@types/node-fetch@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-3.0.3.tgz#9d969c9a748e841554a40ee435d26e53fa3ee899" @@ -840,6 +1498,20 @@ resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz#3336428ec7e9180cf4566dfea5da04eb586a6553" integrity sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q== +"@types/pg@8.6.5": + version "8.6.5" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.6.5.tgz#2dce9cb468a6a5e0f1296a59aea3ac75dd27b702" + integrity sha512-tOkGtAqRVkHa/PVZicq67zuujI4Oorfglsr2IbKofDwBSysnaqSx7W1mDqFqdkGE6Fbgh+PZAl0r/BWON/mozw== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^2.2.0" + +"@types/prettier@^2.1.5": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc" + integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== + "@types/pug@2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" @@ -949,6 +1621,11 @@ dependencies: "@types/node" "*" +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + "@types/tinycolor2@1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706" @@ -1000,6 +1677,18 @@ dependencies: "@types/node" "*" +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.12" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526" + integrity sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@5.36.2": version "5.36.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz#6df092a20e0f9ec748b27f293a12cb39d0c1fe4d" @@ -1081,11 +1770,6 @@ "@typescript-eslint/types" "5.36.2" eslint-visitor-keys "^3.3.0" -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - "@webgpu/types@0.1.16": version "0.1.16" resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.16.tgz#1f05497b95b7c013facf7035c8e21784645f5cc4" @@ -1230,10 +1914,12 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" ansi-regex@^2.0.0: version "2.1.1" @@ -1277,23 +1963,28 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -1368,7 +2059,7 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.10: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -1497,6 +2188,66 @@ axios@^0.24.0: dependencies: follow-redirects "^1.14.4" +babel-jest@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.0.1.tgz#db50de501fc8727e768f5aa417496cb871ee1ba0" + integrity sha512-wyI9r8tqwsZEMWiIaYjdUJ6ztZIO4DMWpGq7laW34wR71WtRS+D/iBEtXOP5W2aSYCVUQMsypRl/xiJYZznnTg== + dependencies: + "@jest/transform" "^29.0.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.0.tgz#ae4873399a199ede93697a15919d3d0f614a2eb1" + integrity sha512-B9oaXrlxXHFWeWqhDPg03iqQd2UN/mg/VdZOsLaqAVBkztru3ctTryAI4zisxLEEgmcUnLTKewqx0gGifoXD3A== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.0.0.tgz#52d7f1afe3a15d14a3c5ab4349cbd388d98d330b" + integrity sha512-B5Ke47Xcs8rDF3p1korT3LoilpADCwbG93ALqtvqu6Xpf4d8alKkrCBTExbNzdHJcIuEPpfYvEaFFRGee2kUgQ== + dependencies: + babel-plugin-jest-hoist "^29.0.0" + babel-preset-current-node-syntax "^1.0.0" + babel-walk@3.0.0-canary-5: version "3.0.0-canary-5" resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" @@ -1620,10 +2371,29 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== +browserslist@^4.20.2: + version "4.21.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" + integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== + dependencies: + caniuse-lite "^1.0.30001370" + electron-to-chromium "^1.4.202" + node-releases "^2.0.6" + update-browserslist-db "^1.0.5" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" @@ -1788,15 +2558,20 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.0.0: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001370: + version "1.0.30001385" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001385.tgz#51d5feeb60b831a5b4c7177f419732060418535c" + integrity sha512-MpiCqJGhBkHgpyimE9GWmZTnyHyEEM35u115bD3QBrXpjvL/JgcP8cUhKJshfmg4OtEHFenifcK5sZayEw5tvQ== canonicalize@^1.0.1: version "1.0.1" @@ -1850,7 +2625,7 @@ chalk@5.0.1: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== -chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1909,7 +2684,22 @@ cheerio@0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@3.5.3, chokidar@^3.3.1, chokidar@^3.5.3: +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.3.1, chokidar@^3.5.3: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -1934,6 +2724,16 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +ci-info@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" + integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2011,6 +2811,11 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + color-convert@2.0.1, color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -2120,6 +2925,11 @@ config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -2159,6 +2969,13 @@ content-type@^1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -2330,13 +3147,6 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2358,6 +3168,13 @@ debug@^4.3.2: dependencies: ms "2.1.2" +debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -2368,11 +3185,6 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - decimal.js@^10.3.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -2385,6 +3197,11 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + deep-email-validator@0.1.21: version "0.1.21" resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.21.tgz#5d0120fe1aeae83ab7cb39378a40a381b681219f" @@ -2480,6 +3297,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204" integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw== +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -2488,10 +3310,10 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff-sequences@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f" + integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA== diff@^4.0.1: version "4.0.2" @@ -2676,6 +3498,16 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" +electron-to-chromium@^1.4.202: + version "1.4.235" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.235.tgz#48ac33c4e869a1795013788099470061463d1890" + integrity sha512-eNU2SmVZYTzYVA5aAWmhAJbdVil5/8H5nMq6kGD0Yxd4k2uKIuT8YmS46I0QXY7iOoPPcb6jjem9/2xyuH5+XQ== + +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2753,6 +3585,13 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -2870,16 +3709,21 @@ escape-regexp@0.0.1: resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254" integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ= -escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -3014,7 +3858,7 @@ espree@^9.4.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -esprima@^4.0.1: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -3083,16 +3927,47 @@ execa@6.1.0: signal-exit "^3.0.7" strip-final-newline "^3.0.0" +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + exit-on-epipe@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expect@^29.0.0, expect@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.1.tgz#a2fa64a59cffe4b4007877e730bc82be3d1742bb" + integrity sha512-yQgemsjLU+1S8t2A7pXT3Sn/v5/37LY8J+tocWtKEA0iEYYc6gfKbbJJX2fxHZmd7K9WpdbQqXUpmYkq1aewYg== + dependencies: + "@jest/expect-utils" "^29.0.1" + jest-get-type "^29.0.0" + jest-matcher-utils "^29.0.1" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -3155,7 +4030,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3165,6 +4040,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-xml-parser@^3.19.0: version "3.19.0" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" @@ -3177,6 +4057,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + feed@4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" @@ -3222,14 +4109,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-up@5.0.0, find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -3237,7 +4116,7 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-up@^4.1.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -3245,6 +4124,14 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3253,11 +4140,6 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - flatted@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" @@ -3365,16 +4247,16 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - fstream@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" @@ -3459,6 +4341,11 @@ generic-pool@3.8.2: resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3473,6 +4360,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-paths@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38" @@ -3509,7 +4401,7 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -get-stream@^6.0.1: +get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== @@ -3555,10 +4447,10 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@7.2.0, glob@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +glob@^7.1.3, glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -3567,10 +4459,10 @@ glob@7.2.0, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -3590,6 +4482,11 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + globals@^13.15.0: version "13.15.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" @@ -3677,6 +4574,11 @@ graceful-fs@^4.2.6: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -3756,11 +4658,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - highlight.js@^10.7.1: version "10.7.2" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" @@ -3788,6 +4685,11 @@ html-entities@2.3.2: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" @@ -3909,6 +4811,11 @@ https-proxy-agent@^5.0.1: agent-base "6" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + human-signals@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" @@ -3975,6 +4882,14 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -4093,6 +5008,11 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -4189,6 +5109,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + is-generator-function@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" @@ -4235,11 +5160,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -4282,6 +5202,11 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" @@ -4331,59 +5256,472 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-weakref@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" + integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== + dependencies: + call-bind "^1.0.0" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-whitespace@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" + integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + +jest-changed-files@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.0.0.tgz#aa238eae42d9372a413dd9a8dadc91ca1806dce0" + integrity sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.0.1.tgz#7ecb4913e134fb4addc03655fb36c9398014fa07" + integrity sha512-I5J4LyK3qPo8EnqPmxsMAVR+2SFx7JOaZsbqW9xQmk4UDmTCD92EQgS162Ey3Jq6CfpKJKFDhzhG3QqiE0fRbw== + dependencies: + "@jest/environment" "^29.0.1" + "@jest/expect" "^29.0.1" + "@jest/test-result" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.0.1" + jest-matcher-utils "^29.0.1" + jest-message-util "^29.0.1" + jest-runtime "^29.0.1" + jest-snapshot "^29.0.1" + jest-util "^29.0.1" + p-limit "^3.1.0" + pretty-format "^29.0.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.0.1.tgz#6633c2ab97337ac5207910bd6b0aba2ef0900110" + integrity sha512-XozBHtoJCS6mnjCxNESyGm47Y4xSWzNlBJj4tix9nGrG6m068B83lrTWKtjYAenYSfOqyYVpQCkyqUp35IT+qA== + dependencies: + "@jest/core" "^29.0.1" + "@jest/test-result" "^29.0.1" + "@jest/types" "^29.0.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.0.1" + jest-util "^29.0.1" + jest-validate "^29.0.1" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.0.1.tgz#bccc2aedc3bafb6cb08bad23e5f0fcc3b1959268" + integrity sha512-3duIx5ucEPIsUOESDTuasMfqHonD0oZRjqHycIMHSC4JwbvHDjAWNKN/NiM0ZxHXjAYrMTLt2QxSQ+IqlbYE5A== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.0.1" + "@jest/types" "^29.0.1" + babel-jest "^29.0.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.0.1" + jest-environment-node "^29.0.1" + jest-get-type "^29.0.0" + jest-regex-util "^29.0.0" + jest-resolve "^29.0.1" + jest-runner "^29.0.1" + jest-util "^29.0.1" + jest-validate "^29.0.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.0.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.1.tgz#d14e900a38ee4798d42feaaf0c61cb5b98e4c028" + integrity sha512-l8PYeq2VhcdxG9tl5cU78ClAlg/N7RtVSp0v3MlXURR0Y99i6eFnegmasOandyTmO6uEdo20+FByAjBFEO9nuw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.0.0" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-docblock@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.0.0.tgz#3151bcc45ed7f5a8af4884dcc049aee699b4ceae" + integrity sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.0.1.tgz#c17da68a7073440122dbd47dca3941351ee0cbe5" + integrity sha512-UmCZYU9LPvRfSDoCrKJqrCNmgTYGGb3Ga6IVsnnVjedBTRRR9GJMca7UmDKRrJ1s+U632xrVtiRD27BxaG1aaQ== + dependencies: + "@jest/types" "^29.0.1" + chalk "^4.0.0" + jest-get-type "^29.0.0" + jest-util "^29.0.1" + pretty-format "^29.0.1" + +jest-environment-node@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.0.1.tgz#b09db2a1b8439aace11a6805719d92498a64987e" + integrity sha512-PcIRBrEBFAPBqkbL53ZpEvTptcAnOW6/lDfqBfACMm3vkVT0N7DcfkH/hqNSbDmSxzGr0FtJI6Ej3TPhveWCMA== + dependencies: + "@jest/environment" "^29.0.1" + "@jest/fake-timers" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + jest-mock "^29.0.1" + jest-util "^29.0.1" + +jest-get-type@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" + integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== + +jest-haste-map@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.0.1.tgz#472212f93ef44309bf97d191f93ddd2e41169615" + integrity sha512-gcKOAydafpGoSBvcj/mGCfhOKO8fRLkAeee1KXGdcJ1Pb9O2nnOl4I8bQSIID2MaZeMHtLLgNboukh/pUGkBtg== + dependencies: + "@jest/types" "^29.0.1" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.0.0" + jest-util "^29.0.1" + jest-worker "^29.0.1" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.0.1.tgz#1a7cf8475d85e7b2bd53efa5adc5195828a12c33" + integrity sha512-5tISHJphB+sCmKXtVHJGQGltj7ksrLLb9vkuNWwFR86Of1tfzjskvrrrZU1gSzEfWC+qXIn4tuh8noKHYGMIPA== + dependencies: + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-matcher-utils@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.1.tgz#eaa92dd5405c2df9d31d45ec4486361d219de3e9" + integrity sha512-/e6UbCDmprRQFnl7+uBKqn4G22c/OmwriE5KCMVqxhElKCQUDcFnq5XM9iJeKtzy4DUjxT27y9VHmKPD8BQPaw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.0.1" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-message-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.1.tgz#85c4b5b90296c228da158e168eaa5b079f2ab879" + integrity sha512-wRMAQt3HrLpxSubdnzOo68QoTfQ+NLXFzU0Heb18ZUzO2S9GgaXNEdQ4rpd0fI9dq2NXkpCk1IUWSqzYKji64A== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.0.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.0.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.1.tgz#12e1b137035365b022ccdb8fd67d476cd4d4bfad" + integrity sha512-i1yTceg2GKJwUNZFjIzrH7Y74fN1SKJWxQX/Vu3LT4TiJerFARH5l+4URNyapZ+DNpchHYrGOP2deVbn3ma8JA== + dependencies: + "@jest/types" "^29.0.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.0.0.tgz#b442987f688289df8eb6c16fa8df488b4cd007de" + integrity sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug== + +jest-resolve-dependencies@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.0.1.tgz#c41b88380c8ea178ce72a750029b5f3d5f65cb94" + integrity sha512-fUGcYlSc1NzNz+tsHDjjG0rclw6blJcFZsLEsezxm/n54bAm9HFvJxgBuCV1CJQoPtIx6AfR+tXkR9lpWJs2LQ== + dependencies: + jest-regex-util "^29.0.0" + jest-snapshot "^29.0.1" + +jest-resolve@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.0.1.tgz#4f1338eee2ccc7319ffce850e13eb118a9e93ce5" + integrity sha512-dwb5Z0lLZbptlBtPExqsHfdDamXeiRLv4vdkfPrN84vBwLSWHWcXjlM2JXD/KLSQfljBcXbzI/PDvUJuTQ84Nw== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.0.1" + jest-pnp-resolver "^1.2.2" + jest-util "^29.0.1" + jest-validate "^29.0.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.0.1.tgz#15bacd13170f3d786168ef8548fdeb96933ea643" + integrity sha512-XeFfPmHtO7HyZyD1uJeO4Oqa8PyTbDHzS1YdGrvsFXk/A5eXinbqA5a42VUEqvsKQgNnKTl5NJD0UtDWg7cQ2A== + dependencies: + "@jest/console" "^29.0.1" + "@jest/environment" "^29.0.1" + "@jest/test-result" "^29.0.1" + "@jest/transform" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.10.2" + graceful-fs "^4.2.9" + jest-docblock "^29.0.0" + jest-environment-node "^29.0.1" + jest-haste-map "^29.0.1" + jest-leak-detector "^29.0.1" + jest-message-util "^29.0.1" + jest-resolve "^29.0.1" + jest-runtime "^29.0.1" + jest-util "^29.0.1" + jest-watcher "^29.0.1" + jest-worker "^29.0.1" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.0.1.tgz#cafdc10834c45c50105eecb0ded8677ce741e2af" + integrity sha512-yDgz5OE0Rm44PUAfTqwA6cDFnTYnVcYbRpPECsokSASQ0I5RXpnKPVr2g0CYZWKzbsXqqtmM7TIk7CAutZJ7gQ== + dependencies: + "@jest/environment" "^29.0.1" + "@jest/fake-timers" "^29.0.1" + "@jest/globals" "^29.0.1" + "@jest/source-map" "^29.0.0" + "@jest/test-result" "^29.0.1" + "@jest/transform" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.0.1" + jest-message-util "^29.0.1" + jest-mock "^29.0.1" + jest-regex-util "^29.0.0" + jest-resolve "^29.0.1" + jest-snapshot "^29.0.1" + jest-util "^29.0.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.0.1.tgz#ed455cb7e56fb43e2d451edd902d622349d6afed" + integrity sha512-OuYGp+lsh7RhB3DDX36z/pzrGm2F740e5ERG9PQpJyDknCRtWdhaehBQyMqDnsQdKkvC2zOcetcxskiHjO7e8Q== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.0.1" + "@jest/transform" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.0.1" + graceful-fs "^4.2.9" + jest-diff "^29.0.1" + jest-get-type "^29.0.0" + jest-haste-map "^29.0.1" + jest-matcher-utils "^29.0.1" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + natural-compare "^1.4.0" + pretty-format "^29.0.1" + semver "^7.3.5" -is-weakref@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" - integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== +jest-util@^28.0.0: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== dependencies: - call-bind "^1.0.0" + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== +jest-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.1.tgz#f854a4a8877c7817316c4afbc2a851ceb2e71598" + integrity sha512-GIWkgNfkeA9d84rORDHPGGTFBrRD13A38QVSKE0bVrGSnoR1KDn8Kqz+0yI5kezMgbT/7zrWaruWP1Kbghlb2A== dependencies: - call-bind "^1.0.2" - -is-whitespace@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" - integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +jest-validate@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.0.1.tgz#8de8ff9d65507c0477964fd39c5b0a1778e3103d" + integrity sha512-mS4q7F738YXZFWBPqE+NjHU/gEOs7IBIFQ8i9zq5EO691cLrUbLhFq4larf8/lNcmauRO71tn/+DTW2y+MrLow== + dependencies: + "@jest/types" "^29.0.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.0.0" + leven "^3.1.0" + pretty-format "^29.0.1" -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +jest-watcher@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.0.1.tgz#63adeb8887a0562ed8f990f413b830ef48a8db94" + integrity sha512-0LBWDL3sZ+vyHRYxjqm2irhfwhUXHonjLSbd0oDeGq44U1e1uUh3icWNXYF8HO/UEnOoa6+OJDncLUXP2Hdg9A== + dependencies: + "@jest/test-result" "^29.0.1" + "@jest/types" "^29.0.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.10.2" + jest-util "^29.0.1" + string-length "^4.0.1" -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== +jest-worker@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.0.1.tgz#fb42ff7e05e0573f330ec0cf781fc545dcd11a31" + integrity sha512-+B/2/8WW7goit7qVezG9vnI1QP3dlmuzi2W0zxazAQQ8dcDIA63dDn6j4pjOGBARha/ZevcwYQtNIzCySbS7fQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" -jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== +jest@29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.0.1.tgz#4a1c48d79fada0a47c686a111ed9411fd41cd584" + integrity sha512-liHkwzaW6iwQyhRBFj0A4ZYKcsQ7ers1s62CCT95fPeNzoxT/vQRWwjTT4e7jpSCwrvPP2t1VESuy7GrXcr2ug== dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" + "@jest/core" "^29.0.1" + "@jest/types" "^29.0.1" + import-local "^3.0.2" + jest-cli "^29.0.1" jmespath@0.16.0: version "0.16.0" @@ -4421,6 +5759,11 @@ js-stringify@^1.0.2: resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds= +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -4428,6 +5771,14 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@1.1.0, jsbn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -4476,11 +5827,21 @@ jsdom@20.0.0: ws "^8.8.0" xml-name-validator "^4.0.0" +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4621,6 +5982,11 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.1.5" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + koa-bodyparser@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#274c778555ff48fa221ee7f36a9fbdbace22759a" @@ -4786,6 +6152,11 @@ lazystream@^1.0.0: dependencies: readable-stream "^2.0.5" +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4802,6 +6173,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + listenercount@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" @@ -4898,6 +6274,11 @@ lodash.map@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.merge@^4.4.0, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -4933,14 +6314,6 @@ lodash@^4.17.11, lodash@^4.17.19, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - long@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -4986,14 +6359,14 @@ mailcheck@^1.1.1: resolved "https://registry.yarnpkg.com/mailcheck/-/mailcheck-1.1.1.tgz#d87cf6ba0b64ba512199dbf93f1489f479591e34" integrity sha1-2Hz2ugtkulEhmdv5PxSJ9HlZHjQ= -make-dir@^3.1.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -5020,6 +6393,13 @@ make-fetch-happen@^10.0.3: socks-proxy-agent "^7.0.0" ssri "^9.0.0" +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5087,6 +6467,11 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24: dependencies: mime-db "1.44.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-fn@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" @@ -5107,13 +6492,6 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@5.0.1, minimatch@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5121,6 +6499,13 @@ minimatch@^3.0.4, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -5240,34 +6625,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mocha@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" - integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.4" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.2.0" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "5.0.1" - ms "2.1.3" - nanoid "3.3.3" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - workerpool "6.2.1" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - moment@^2.22.2: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -5283,16 +6640,16 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@3.0.0-canary.1: version "3.0.0-canary.1" resolved "https://registry.yarnpkg.com/ms/-/ms-3.0.0-canary.1.tgz#c7b34fbce381492fd0b345d1cf56e14d67b77b80" integrity sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g== +ms@^2.0.0, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + msgpackr-extract@^1.0.14: version "1.0.16" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.16.tgz#701c4f6e6f25c100ae84557092274e8fffeefe45" @@ -5351,11 +6708,6 @@ nan@^2.16.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== -nanoid@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" - integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== - nanoid@^3.1.30: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" @@ -5518,6 +6870,16 @@ node-gyp@^9.0.0: tar "^6.1.2" which "^2.0.2" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + nodemailer@6.7.8: version "6.7.8" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.8.tgz#9f1af9911314960c0b889079e1754e8d9e3f740a" @@ -5560,6 +6922,13 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npm-run-path@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" @@ -5636,6 +7005,11 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-hash@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.11.0, object-inspect@^1.9.0: version "1.11.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" @@ -5689,6 +7063,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + onetime@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" @@ -5789,6 +7170,13 @@ p-limit@^3.0.2: dependencies: p-try "^2.0.0" +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -5858,6 +7246,16 @@ parse-data-uri@^0.2.0: dependencies: data-uri-to-buffer "0.0.3" +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -5919,7 +7317,7 @@ path-is-absolute@1.0.1, path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -5934,6 +7332,11 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + path-to-regexp@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427" @@ -5969,12 +7372,12 @@ pg-pool@^3.5.2: resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.2.tgz#ed1bed1fb8d79f1c6fd5fb1c99e990fbf9ddf178" integrity sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w== -pg-protocol@^1.5.0: +pg-protocol@*, pg-protocol@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== -pg-types@^2.1.0: +pg-types@^2.1.0, pg-types@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== @@ -6015,7 +7418,7 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -picomatch@^2.3.1: +picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -6025,6 +7428,18 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + plimit-lit@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.2.6.tgz#8c1336f26a042b6e9f1acc665be5eee4c2a55fb3" @@ -6112,6 +7527,15 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-format@^29.0.0, pretty-format@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.1.tgz#2f8077114cdac92a59b464292972a106410c7ad0" + integrity sha512-iTHy3QZMzuL484mSTYbQIM1AHhEQsH8mXWS2/vd2yFBYnG3EBqGiMONo28PlPgrW7P/8s/1ISv+y7WH306l8cw== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5" @@ -6190,6 +7614,14 @@ promise@^7.0.1: dependencies: asap "~2.0.3" +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -6382,13 +7814,6 @@ random-seed@0.3.0: dependencies: json-stringify-safe "^5.0.1" -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - rangestr@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/rangestr/-/rangestr-0.0.1.tgz#f72ff9246f10f2a7d7c16e14616f617be2c2635a" @@ -6435,6 +7860,11 @@ re2@1.17.7: nan "^2.16.0" node-gyp "^9.0.0" +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + readable-stream@1.1.x, readable-stream@~1.1.9: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -6627,11 +8057,23 @@ resolve-alpn@^1.2.0: resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-path@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" @@ -6640,6 +8082,11 @@ resolve-path@^1.4.0: http-errors "~1.6.2" path-is-absolute "1.0.1" +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + resolve@^1.15.1, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -6709,6 +8156,13 @@ run-parallel@^1.1.9: resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== +rxjs@7.5.6: + version "7.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" + integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + dependencies: + tslib "^2.1.0" + s-age@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/s-age/-/s-age-1.1.2.tgz#c0cf15233ccc93f41de92ea42c36d957977d1ea2" @@ -6724,7 +8178,7 @@ safe-buffer@5.2.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -6787,7 +8241,7 @@ seedrandom@3.0.5, seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -semver@7.3.7, semver@^7.3.7: +semver@7.3.7, semver@7.x, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== @@ -6799,7 +8253,7 @@ semver@^5.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -6818,13 +8272,6 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -6898,7 +8345,7 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -signal-exit@^3.0.7: +signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -6924,6 +8371,11 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -6956,7 +8408,15 @@ source-map-js@^0.6.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== -source-map@~0.6.1: +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -7022,6 +8482,13 @@ ssri@^9.0.0: dependencies: minipass "^3.1.1" +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" @@ -7049,6 +8516,14 @@ strict-event-emitter-types@2.0.0: resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -7177,12 +8652,22 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-final-newline@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -7217,13 +8702,6 @@ summaly@2.7.0: require-all "3.0.0" trace-redirect "1.0.6" -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7231,13 +8709,28 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -7332,6 +8825,23 @@ tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7373,6 +8883,11 @@ tmp@0.2.1: dependencies: rimraf "^3.0.0" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -7442,6 +8957,20 @@ trace-redirect@1.0.6: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= +ts-jest@28.0.8: + version "28.0.8" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.8.tgz#cd204b8e7a2f78da32cf6c95c9a6165c5b99cc73" + integrity sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^28.0.0" + json5 "^2.2.1" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + ts-loader@9.3.1: version "9.3.1" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4" @@ -7502,6 +9031,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.4.0, tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -7565,6 +9099,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.4: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -7698,6 +9237,14 @@ unzipper@0.10.11: readable-stream "~2.3.6" setimmediate "~1.0.4" +update-browserslist-db@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" + integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -7752,6 +9299,11 @@ uuid@8.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== +uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" @@ -7762,16 +9314,20 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + vary@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -7805,6 +9361,13 @@ w3c-xmlserializer@^3.0.0: dependencies: xml-name-validator "^4.0.0" +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + web-push@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.5.0.tgz#4576533746052eda3bd50414b54a1b0a21eeaeae" @@ -7943,11 +9506,6 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" - integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== - wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -7971,6 +9529,14 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + ws@8.8.1: version "8.8.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" @@ -8069,11 +9635,6 @@ yaml-ast-parser@0.0.43: resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yargs-parser@20.2.4, yargs-parser@^20.2.2: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -8082,33 +9643,20 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@^21.0.0: version "21.0.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== -yargs-unparser@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@16.2.0, yargs@^16.0.0, yargs@^16.0.3: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" +yargs-parser@^21.0.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^15.3.1: version "15.4.1" @@ -8127,6 +9675,19 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.0.0, yargs@^16.0.3: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.3.1: version "17.4.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.0.tgz#9fc9efc96bd3aa2c1240446af28499f0e7593d00" @@ -8150,6 +9711,11 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zip-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index a5a4fd0f43..01dedd1c69 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -21,6 +21,9 @@ module.exports = { 'allowSingleExtends': true, }, ], + '@typescript-eslint/prefer-nullish-coalescing': [ + 'error', + ], // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため 'id-denylist': ['error', 'window', 'e'], diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts index 01b8fd05fe..6debcb8a13 100644 --- a/packages/client/src/scripts/aiscript/api.ts +++ b/packages/client/src/scripts/aiscript/api.ts @@ -27,7 +27,7 @@ export function createAiScriptEnv(opts) { if (token) utils.assertString(token); apiRequests++; if (apiRequests > 16) return values.NULL; - const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); + const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); return utils.jsToVal(res); }), 'Mk:save': values.FN_NATIVE(([key, value]) => { diff --git a/packages/client/src/scripts/hpml/type-checker.ts b/packages/client/src/scripts/hpml/type-checker.ts index 9633b3cd01..24c9ed8bcb 100644 --- a/packages/client/src/scripts/hpml/type-checker.ts +++ b/packages/client/src/scripts/hpml/type-checker.ts @@ -1,7 +1,9 @@ import autobind from 'autobind-decorator'; -import { Type, envVarsDef, PageVar } from '.'; -import { Expr, isLiteralValue, Variable } from './expr'; +import { isLiteralValue } from './expr'; import { funcDefs } from './lib'; +import { envVarsDef } from '.'; +import type { Type, PageVar } from '.'; +import type { Expr, Variable } from './expr'; type TypeError = { arg: number; @@ -44,14 +46,14 @@ export class HpmlTypeChecker { return { arg: i, expect: generic[arg], - actual: type + actual: type, }; } } else if (type !== arg) { return { arg: i, expect: arg, - actual: type + actual: type, }; } } @@ -81,7 +83,7 @@ export class HpmlTypeChecker { } if (typeof def.in[slot] === 'number') { - return generic[def.in[slot]] || null; + return generic[def.in[slot]] ?? null; } else { return def.in[slot]; } diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 5a6a9c5af9..2952b8ba16 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -24,6 +24,8 @@ module.exports = { 'semi-spacing': ['error', { 'before': false, 'after': true }], 'quotes': ['warn', 'single'], 'comma-dangle': ['warn', 'always-multiline'], + 'comma-spacing': ['error', { 'before': false, 'after': true }], + 'array-bracket-spacing': ['error', 'never'], 'keyword-spacing': ['error', { 'before': true, 'after': true, @@ -72,6 +74,9 @@ module.exports = { 'checksVoidReturn': false, }], '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/prefer-nullish-coalescing': [ + 'error', + ], 'import/no-unresolved': ['off'], 'import/no-default-export': ['warn'], 'import/order': ['warn', { diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts index 0404e21e57..8350fb80b8 100644 --- a/packages/sw/src/types.ts +++ b/packages/sw/src/types.ts @@ -10,7 +10,7 @@ export type SwMessage = { [x: string]: any; }; -// Defined also @/services/push-notification.ts#L7-L14 +// Defined also @/core/push-notification.ts#L7-L14 type pushNotificationDataSourceMap = { notification: Misskey.entities.Notification; unreadMessagingMessage: Misskey.entities.MessagingMessage; -- cgit v1.2.3-freya From 01d4d55e78fd977b9d44a68a2504e6091d346b7a Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 21 Sep 2022 05:33:11 +0900 Subject: fix import type --- packages/backend/src/core/AccountUpdateService.ts | 4 ++-- packages/backend/src/core/AiService.ts | 2 +- packages/backend/src/core/AntennaService.ts | 2 +- packages/backend/src/core/CaptchaService.ts | 2 +- packages/backend/src/core/CreateNotificationService.ts | 2 +- packages/backend/src/core/CustomEmojiService.ts | 4 ++-- packages/backend/src/core/DeleteAccountService.ts | 2 +- packages/backend/src/core/DownloadService.ts | 2 +- packages/backend/src/core/DriveService.ts | 4 ++-- packages/backend/src/core/EmailService.ts | 4 ++-- packages/backend/src/core/FederatedInstanceService.ts | 2 +- packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +- packages/backend/src/core/GlobalEventService.ts | 2 +- packages/backend/src/core/HashtagService.ts | 2 +- packages/backend/src/core/HttpRequestService.ts | 2 +- packages/backend/src/core/IdService.ts | 2 +- packages/backend/src/core/ImageProcessingService.ts | 2 +- packages/backend/src/core/InstanceActorService.ts | 2 +- packages/backend/src/core/InternalStorageService.ts | 2 +- packages/backend/src/core/LoggerService.ts | 2 +- packages/backend/src/core/MessagingService.ts | 4 ++-- packages/backend/src/core/MfmService.ts | 2 +- packages/backend/src/core/ModerationLogService.ts | 2 +- packages/backend/src/core/NoteCreateService.ts | 4 ++-- packages/backend/src/core/NoteDeleteService.ts | 4 ++-- packages/backend/src/core/NotePiningService.ts | 4 ++-- packages/backend/src/core/NotificationService.ts | 2 +- packages/backend/src/core/PollService.ts | 2 +- packages/backend/src/core/ProxyAccountService.ts | 2 +- packages/backend/src/core/PushNotificationService.ts | 4 ++-- packages/backend/src/core/QueueService.ts | 4 ++-- packages/backend/src/core/ReactionService.ts | 2 +- packages/backend/src/core/RelayService.ts | 2 +- packages/backend/src/core/S3Service.ts | 2 +- packages/backend/src/core/SignupService.ts | 4 ++-- packages/backend/src/core/TwoFactorAuthenticationService.ts | 4 ++-- packages/backend/src/core/UserCacheService.ts | 2 +- packages/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 | 4 ++-- packages/backend/src/core/UtilityService.ts | 2 +- packages/backend/src/core/VideoProcessingService.ts | 2 +- packages/backend/src/core/WebhookService.ts | 2 +- packages/backend/src/core/chart/charts/federation.ts | 2 +- packages/backend/src/core/chart/charts/instance.ts | 2 +- packages/backend/src/core/chart/charts/notes.ts | 2 +- packages/backend/src/core/chart/charts/per-user-drive.ts | 2 +- packages/backend/src/core/chart/charts/per-user-following.ts | 2 +- packages/backend/src/core/chart/charts/per-user-notes.ts | 2 +- packages/backend/src/core/chart/charts/users.ts | 2 +- packages/backend/src/core/entities/AbuseUserReportEntityService.ts | 2 +- packages/backend/src/core/entities/AntennaEntityService.ts | 2 +- packages/backend/src/core/entities/AppEntityService.ts | 2 +- packages/backend/src/core/entities/AuthSessionEntityService.ts | 2 +- packages/backend/src/core/entities/BlockingEntityService.ts | 2 +- packages/backend/src/core/entities/ChannelEntityService.ts | 2 +- packages/backend/src/core/entities/ClipEntityService.ts | 2 +- packages/backend/src/core/entities/DriveFileEntityService.ts | 4 ++-- packages/backend/src/core/entities/DriveFolderEntityService.ts | 2 +- packages/backend/src/core/entities/EmojiEntityService.ts | 2 +- packages/backend/src/core/entities/FollowRequestEntityService.ts | 2 +- packages/backend/src/core/entities/FollowingEntityService.ts | 2 +- packages/backend/src/core/entities/GalleryLikeEntityService.ts | 2 +- packages/backend/src/core/entities/GalleryPostEntityService.ts | 2 +- packages/backend/src/core/entities/HashtagEntityService.ts | 2 +- packages/backend/src/core/entities/InstanceEntityService.ts | 2 +- packages/backend/src/core/entities/MessagingMessageEntityService.ts | 2 +- packages/backend/src/core/entities/ModerationLogEntityService.ts | 2 +- packages/backend/src/core/entities/MutingEntityService.ts | 2 +- packages/backend/src/core/entities/NoteEntityService.ts | 2 +- packages/backend/src/core/entities/NoteFavoriteEntityService.ts | 2 +- packages/backend/src/core/entities/NoteReactionEntityService.ts | 2 +- packages/backend/src/core/entities/NotificationEntityService.ts | 2 +- packages/backend/src/core/entities/PageEntityService.ts | 2 +- packages/backend/src/core/entities/PageLikeEntityService.ts | 2 +- packages/backend/src/core/entities/SigninEntityService.ts | 2 +- packages/backend/src/core/entities/UserEntityService.ts | 4 ++-- packages/backend/src/core/entities/UserGroupEntityService.ts | 2 +- .../backend/src/core/entities/UserGroupInvitationEntityService.ts | 2 +- packages/backend/src/core/entities/UserListEntityService.ts | 2 +- packages/backend/src/core/remote/ResolveUserService.ts | 4 ++-- packages/backend/src/core/remote/WebfingerService.ts | 2 +- packages/backend/src/core/remote/activitypub/ApDbResolverService.ts | 4 ++-- .../backend/src/core/remote/activitypub/ApDeliverManagerService.ts | 4 ++-- packages/backend/src/core/remote/activitypub/ApInboxService.ts | 2 +- packages/backend/src/core/remote/activitypub/ApMfmService.ts | 2 +- packages/backend/src/core/remote/activitypub/ApRendererService.ts | 2 +- packages/backend/src/core/remote/activitypub/ApRequestService.ts | 2 +- packages/backend/src/core/remote/activitypub/ApResolverService.ts | 4 ++-- .../backend/src/core/remote/activitypub/models/ApImageService.ts | 4 ++-- .../backend/src/core/remote/activitypub/models/ApMentionService.ts | 2 +- packages/backend/src/core/remote/activitypub/models/ApNoteService.ts | 4 ++-- .../backend/src/core/remote/activitypub/models/ApPersonService.ts | 4 ++-- .../backend/src/core/remote/activitypub/models/ApQuestionService.ts | 4 ++-- packages/backend/src/daemons/JanitorService.ts | 2 +- packages/backend/src/queue/DbQueueProcessorsService.ts | 2 +- packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts | 2 +- packages/backend/src/queue/QueueProcessorService.ts | 2 +- packages/backend/src/queue/SystemQueueProcessorsService.ts | 2 +- .../src/queue/processors/CheckExpiredMutingsProcessorService.ts | 4 ++-- packages/backend/src/queue/processors/CleanChartsProcessorService.ts | 2 +- packages/backend/src/queue/processors/CleanProcessorService.ts | 4 ++-- .../backend/src/queue/processors/CleanRemoteFilesProcessorService.ts | 4 ++-- .../backend/src/queue/processors/DeleteAccountProcessorService.ts | 4 ++-- .../backend/src/queue/processors/DeleteDriveFilesProcessorService.ts | 4 ++-- packages/backend/src/queue/processors/DeleteFileProcessorService.ts | 2 +- packages/backend/src/queue/processors/DeliverProcessorService.ts | 4 ++-- .../src/queue/processors/EndedPollNotificationProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ExportBlockingProcessorService.ts | 5 ++--- .../src/queue/processors/ExportCustomEmojisProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ExportFollowingProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ExportMutingProcessorService.ts | 4 ++-- packages/backend/src/queue/processors/ExportNotesProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ExportUserListsProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ImportBlockingProcessorService.ts | 4 ++-- .../src/queue/processors/ImportCustomEmojisProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ImportFollowingProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ImportMutingProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ImportUserListsProcessorService.ts | 4 ++-- packages/backend/src/queue/processors/InboxProcessorService.ts | 4 ++-- .../backend/src/queue/processors/ResyncChartsProcessorService.ts | 2 +- packages/backend/src/queue/processors/TickChartsProcessorService.ts | 2 +- .../backend/src/queue/processors/WebhookDeliverProcessorService.ts | 4 ++-- packages/backend/src/server/ActivityPubServerService.ts | 4 ++-- packages/backend/src/server/FileServerService.ts | 4 ++-- packages/backend/src/server/MediaProxyServerService.ts | 2 +- packages/backend/src/server/NodeinfoServerService.ts | 4 ++-- packages/backend/src/server/ServerService.ts | 4 ++-- packages/backend/src/server/WellKnownServerService.ts | 4 ++-- packages/backend/src/server/api/ApiCallService.ts | 2 +- packages/backend/src/server/api/ApiServerService.ts | 4 ++-- packages/backend/src/server/api/AuthenticateService.ts | 2 +- packages/backend/src/server/api/SigninApiService.ts | 4 ++-- packages/backend/src/server/api/SigninService.ts | 4 ++-- packages/backend/src/server/api/SignupApiService.ts | 4 ++-- packages/backend/src/server/api/StreamingApiServerService.ts | 4 ++-- packages/backend/src/server/api/common/GetterService.ts | 2 +- .../backend/src/server/api/endpoints/admin/abuse-user-reports.ts | 2 +- packages/backend/src/server/api/endpoints/admin/accounts/create.ts | 2 +- packages/backend/src/server/api/endpoints/admin/accounts/delete.ts | 2 +- packages/backend/src/server/api/endpoints/admin/ad/create.ts | 2 +- packages/backend/src/server/api/endpoints/admin/ad/delete.ts | 2 +- packages/backend/src/server/api/endpoints/admin/ad/list.ts | 2 +- packages/backend/src/server/api/endpoints/admin/ad/update.ts | 2 +- .../backend/src/server/api/endpoints/admin/announcements/create.ts | 2 +- .../backend/src/server/api/endpoints/admin/announcements/delete.ts | 2 +- .../backend/src/server/api/endpoints/admin/announcements/list.ts | 2 +- .../backend/src/server/api/endpoints/admin/announcements/update.ts | 2 +- packages/backend/src/server/api/endpoints/admin/delete-account.ts | 2 +- .../src/server/api/endpoints/admin/delete-all-files-of-a-user.ts | 2 +- .../src/server/api/endpoints/admin/drive-capacity-override.ts | 2 +- packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts | 2 +- packages/backend/src/server/api/endpoints/admin/drive/files.ts | 2 +- packages/backend/src/server/api/endpoints/admin/drive/show-file.ts | 2 +- .../backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/add.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/copy.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/delete.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/list.ts | 2 +- .../src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts | 2 +- .../backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts | 2 +- .../src/server/api/endpoints/admin/emoji/set-category-bulk.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/update.ts | 2 +- .../src/server/api/endpoints/admin/federation/delete-all-files.ts | 2 +- .../endpoints/admin/federation/refresh-remote-instance-metadata.ts | 2 +- .../server/api/endpoints/admin/federation/remove-all-following.ts | 2 +- .../src/server/api/endpoints/admin/federation/update-instance.ts | 2 +- packages/backend/src/server/api/endpoints/admin/get-user-ips.ts | 2 +- packages/backend/src/server/api/endpoints/admin/invite.ts | 2 +- packages/backend/src/server/api/endpoints/admin/meta.ts | 2 +- packages/backend/src/server/api/endpoints/admin/moderators/add.ts | 2 +- packages/backend/src/server/api/endpoints/admin/moderators/remove.ts | 2 +- packages/backend/src/server/api/endpoints/admin/promo/create.ts | 2 +- packages/backend/src/server/api/endpoints/admin/reset-password.ts | 2 +- .../src/server/api/endpoints/admin/resolve-abuse-user-report.ts | 2 +- .../backend/src/server/api/endpoints/admin/show-moderation-logs.ts | 2 +- packages/backend/src/server/api/endpoints/admin/show-user.ts | 2 +- packages/backend/src/server/api/endpoints/admin/show-users.ts | 2 +- packages/backend/src/server/api/endpoints/admin/silence-user.ts | 2 +- packages/backend/src/server/api/endpoints/admin/suspend-user.ts | 2 +- packages/backend/src/server/api/endpoints/admin/unsilence-user.ts | 2 +- packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts | 2 +- packages/backend/src/server/api/endpoints/admin/update-user-note.ts | 2 +- packages/backend/src/server/api/endpoints/announcements.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/create.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/delete.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/list.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/notes.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/show.ts | 2 +- packages/backend/src/server/api/endpoints/antennas/update.ts | 2 +- packages/backend/src/server/api/endpoints/ap/show.ts | 2 +- packages/backend/src/server/api/endpoints/app/create.ts | 2 +- packages/backend/src/server/api/endpoints/app/show.ts | 2 +- packages/backend/src/server/api/endpoints/auth/accept.ts | 2 +- packages/backend/src/server/api/endpoints/auth/session/generate.ts | 4 ++-- packages/backend/src/server/api/endpoints/auth/session/show.ts | 2 +- packages/backend/src/server/api/endpoints/auth/session/userkey.ts | 2 +- packages/backend/src/server/api/endpoints/blocking/create.ts | 2 +- packages/backend/src/server/api/endpoints/blocking/delete.ts | 2 +- packages/backend/src/server/api/endpoints/blocking/list.ts | 2 +- packages/backend/src/server/api/endpoints/channels/create.ts | 2 +- packages/backend/src/server/api/endpoints/channels/featured.ts | 2 +- packages/backend/src/server/api/endpoints/channels/follow.ts | 2 +- packages/backend/src/server/api/endpoints/channels/followed.ts | 2 +- packages/backend/src/server/api/endpoints/channels/owned.ts | 2 +- packages/backend/src/server/api/endpoints/channels/show.ts | 2 +- packages/backend/src/server/api/endpoints/channels/timeline.ts | 2 +- packages/backend/src/server/api/endpoints/channels/unfollow.ts | 2 +- packages/backend/src/server/api/endpoints/channels/update.ts | 2 +- packages/backend/src/server/api/endpoints/clips/add-note.ts | 2 +- packages/backend/src/server/api/endpoints/clips/create.ts | 2 +- packages/backend/src/server/api/endpoints/clips/delete.ts | 2 +- packages/backend/src/server/api/endpoints/clips/list.ts | 2 +- packages/backend/src/server/api/endpoints/clips/notes.ts | 2 +- packages/backend/src/server/api/endpoints/clips/remove-note.ts | 2 +- packages/backend/src/server/api/endpoints/clips/show.ts | 2 +- packages/backend/src/server/api/endpoints/clips/update.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files.ts | 2 +- .../backend/src/server/api/endpoints/drive/files/attached-notes.ts | 2 +- .../backend/src/server/api/endpoints/drive/files/check-existence.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/create.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/delete.ts | 2 +- .../backend/src/server/api/endpoints/drive/files/find-by-hash.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/find.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/show.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/update.ts | 2 +- .../backend/src/server/api/endpoints/drive/files/upload-from-url.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders/create.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders/delete.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders/find.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders/show.ts | 2 +- packages/backend/src/server/api/endpoints/drive/folders/update.ts | 2 +- packages/backend/src/server/api/endpoints/drive/stream.ts | 2 +- packages/backend/src/server/api/endpoints/federation/followers.ts | 2 +- packages/backend/src/server/api/endpoints/federation/following.ts | 2 +- packages/backend/src/server/api/endpoints/federation/instances.ts | 2 +- .../backend/src/server/api/endpoints/federation/show-instance.ts | 2 +- packages/backend/src/server/api/endpoints/federation/stats.ts | 2 +- packages/backend/src/server/api/endpoints/federation/users.ts | 2 +- packages/backend/src/server/api/endpoints/fetch-rss.ts | 2 +- packages/backend/src/server/api/endpoints/following/create.ts | 2 +- packages/backend/src/server/api/endpoints/following/delete.ts | 2 +- packages/backend/src/server/api/endpoints/following/invalidate.ts | 2 +- .../backend/src/server/api/endpoints/following/requests/cancel.ts | 2 +- packages/backend/src/server/api/endpoints/following/requests/list.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/featured.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/popular.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/create.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/delete.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/like.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/show.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts | 2 +- packages/backend/src/server/api/endpoints/gallery/posts/update.ts | 2 +- packages/backend/src/server/api/endpoints/get-online-users-count.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/list.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/search.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/show.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/trend.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/users.ts | 2 +- packages/backend/src/server/api/endpoints/i.ts | 2 +- packages/backend/src/server/api/endpoints/i/2fa/done.ts | 2 +- packages/backend/src/server/api/endpoints/i/2fa/key-done.ts | 4 ++-- packages/backend/src/server/api/endpoints/i/2fa/password-less.ts | 2 +- packages/backend/src/server/api/endpoints/i/2fa/register-key.ts | 2 +- packages/backend/src/server/api/endpoints/i/2fa/register.ts | 4 ++-- packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts | 2 +- packages/backend/src/server/api/endpoints/i/2fa/unregister.ts | 2 +- packages/backend/src/server/api/endpoints/i/apps.ts | 2 +- packages/backend/src/server/api/endpoints/i/authorized-apps.ts | 2 +- packages/backend/src/server/api/endpoints/i/change-password.ts | 2 +- packages/backend/src/server/api/endpoints/i/delete-account.ts | 2 +- packages/backend/src/server/api/endpoints/i/favorites.ts | 2 +- packages/backend/src/server/api/endpoints/i/gallery/likes.ts | 2 +- packages/backend/src/server/api/endpoints/i/gallery/posts.ts | 2 +- .../backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-blocking.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-following.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-muting.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-user-lists.ts | 2 +- packages/backend/src/server/api/endpoints/i/notifications.ts | 2 +- packages/backend/src/server/api/endpoints/i/page-likes.ts | 2 +- packages/backend/src/server/api/endpoints/i/pages.ts | 2 +- .../src/server/api/endpoints/i/read-all-messaging-messages.ts | 2 +- packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts | 2 +- packages/backend/src/server/api/endpoints/i/read-announcement.ts | 2 +- packages/backend/src/server/api/endpoints/i/regenerate-token.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/get-all.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/get-detail.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/get.ts | 2 +- .../backend/src/server/api/endpoints/i/registry/keys-with-type.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/keys.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/remove.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/scopes.ts | 2 +- packages/backend/src/server/api/endpoints/i/registry/set.ts | 2 +- packages/backend/src/server/api/endpoints/i/revoke-token.ts | 2 +- packages/backend/src/server/api/endpoints/i/signin-history.ts | 2 +- packages/backend/src/server/api/endpoints/i/update-email.ts | 4 ++-- packages/backend/src/server/api/endpoints/i/update.ts | 2 +- packages/backend/src/server/api/endpoints/i/user-group-invites.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/create.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/delete.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/list.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/show.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/update.ts | 2 +- packages/backend/src/server/api/endpoints/messaging/history.ts | 2 +- packages/backend/src/server/api/endpoints/messaging/messages.ts | 2 +- .../backend/src/server/api/endpoints/messaging/messages/create.ts | 2 +- .../backend/src/server/api/endpoints/messaging/messages/delete.ts | 2 +- packages/backend/src/server/api/endpoints/messaging/messages/read.ts | 2 +- packages/backend/src/server/api/endpoints/meta.ts | 4 ++-- packages/backend/src/server/api/endpoints/miauth/gen-token.ts | 2 +- packages/backend/src/server/api/endpoints/mute/create.ts | 2 +- packages/backend/src/server/api/endpoints/mute/delete.ts | 2 +- packages/backend/src/server/api/endpoints/mute/list.ts | 2 +- packages/backend/src/server/api/endpoints/my/apps.ts | 2 +- packages/backend/src/server/api/endpoints/notes.ts | 2 +- packages/backend/src/server/api/endpoints/notes/children.ts | 2 +- packages/backend/src/server/api/endpoints/notes/clips.ts | 2 +- packages/backend/src/server/api/endpoints/notes/conversation.ts | 2 +- packages/backend/src/server/api/endpoints/notes/create.ts | 2 +- packages/backend/src/server/api/endpoints/notes/delete.ts | 2 +- packages/backend/src/server/api/endpoints/notes/favorites/create.ts | 2 +- packages/backend/src/server/api/endpoints/notes/favorites/delete.ts | 2 +- packages/backend/src/server/api/endpoints/notes/featured.ts | 2 +- packages/backend/src/server/api/endpoints/notes/global-timeline.ts | 2 +- packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts | 2 +- packages/backend/src/server/api/endpoints/notes/local-timeline.ts | 2 +- packages/backend/src/server/api/endpoints/notes/mentions.ts | 2 +- .../backend/src/server/api/endpoints/notes/polls/recommendation.ts | 2 +- packages/backend/src/server/api/endpoints/notes/polls/vote.ts | 2 +- packages/backend/src/server/api/endpoints/notes/reactions.ts | 2 +- packages/backend/src/server/api/endpoints/notes/renotes.ts | 2 +- packages/backend/src/server/api/endpoints/notes/replies.ts | 2 +- packages/backend/src/server/api/endpoints/notes/search-by-tag.ts | 2 +- packages/backend/src/server/api/endpoints/notes/search.ts | 4 ++-- packages/backend/src/server/api/endpoints/notes/show.ts | 2 +- packages/backend/src/server/api/endpoints/notes/state.ts | 2 +- .../backend/src/server/api/endpoints/notes/thread-muting/create.ts | 2 +- .../backend/src/server/api/endpoints/notes/thread-muting/delete.ts | 2 +- packages/backend/src/server/api/endpoints/notes/timeline.ts | 2 +- packages/backend/src/server/api/endpoints/notes/translate.ts | 4 ++-- packages/backend/src/server/api/endpoints/notes/unrenote.ts | 2 +- .../backend/src/server/api/endpoints/notes/user-list-timeline.ts | 2 +- .../src/server/api/endpoints/notifications/mark-all-as-read.ts | 2 +- packages/backend/src/server/api/endpoints/page-push.ts | 2 +- packages/backend/src/server/api/endpoints/pages/create.ts | 2 +- packages/backend/src/server/api/endpoints/pages/delete.ts | 2 +- packages/backend/src/server/api/endpoints/pages/featured.ts | 2 +- packages/backend/src/server/api/endpoints/pages/like.ts | 2 +- packages/backend/src/server/api/endpoints/pages/show.ts | 2 +- packages/backend/src/server/api/endpoints/pages/unlike.ts | 2 +- packages/backend/src/server/api/endpoints/pages/update.ts | 2 +- packages/backend/src/server/api/endpoints/pinned-users.ts | 2 +- packages/backend/src/server/api/endpoints/promo/read.ts | 2 +- packages/backend/src/server/api/endpoints/request-reset-password.ts | 4 ++-- packages/backend/src/server/api/endpoints/reset-password.ts | 2 +- packages/backend/src/server/api/endpoints/stats.ts | 2 +- packages/backend/src/server/api/endpoints/sw/register.ts | 2 +- packages/backend/src/server/api/endpoints/sw/unregister.ts | 2 +- packages/backend/src/server/api/endpoints/username/available.ts | 2 +- packages/backend/src/server/api/endpoints/users.ts | 2 +- packages/backend/src/server/api/endpoints/users/clips.ts | 2 +- packages/backend/src/server/api/endpoints/users/followers.ts | 2 +- packages/backend/src/server/api/endpoints/users/following.ts | 2 +- packages/backend/src/server/api/endpoints/users/gallery/posts.ts | 2 +- .../src/server/api/endpoints/users/get-frequently-replied-users.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/create.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/delete.ts | 2 +- .../src/server/api/endpoints/users/groups/invitations/accept.ts | 2 +- .../src/server/api/endpoints/users/groups/invitations/reject.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/invite.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/joined.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/leave.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/owned.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/pull.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/show.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/transfer.ts | 2 +- packages/backend/src/server/api/endpoints/users/groups/update.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/create.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/delete.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/list.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/pull.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/push.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/show.ts | 2 +- packages/backend/src/server/api/endpoints/users/lists/update.ts | 2 +- packages/backend/src/server/api/endpoints/users/notes.ts | 2 +- packages/backend/src/server/api/endpoints/users/pages.ts | 2 +- packages/backend/src/server/api/endpoints/users/reactions.ts | 2 +- packages/backend/src/server/api/endpoints/users/recommendation.ts | 2 +- packages/backend/src/server/api/endpoints/users/relation.ts | 2 +- packages/backend/src/server/api/endpoints/users/report-abuse.ts | 2 +- .../src/server/api/endpoints/users/search-by-username-and-host.ts | 2 +- packages/backend/src/server/api/endpoints/users/search.ts | 2 +- packages/backend/src/server/api/endpoints/users/show.ts | 2 +- packages/backend/src/server/api/integration/DiscordServerService.ts | 4 ++-- packages/backend/src/server/api/integration/GithubServerService.ts | 4 ++-- packages/backend/src/server/api/integration/TwitterServerService.ts | 4 ++-- packages/backend/src/server/api/stream/channels/messaging.ts | 2 +- packages/backend/src/server/api/stream/channels/user-list.ts | 2 +- packages/backend/src/server/web/ClientServerService.ts | 2 +- packages/backend/src/server/web/FeedService.ts | 4 ++-- packages/backend/src/server/web/UrlPreviewService.ts | 4 ++-- packages/shared/.eslintrc.js | 2 +- 408 files changed, 474 insertions(+), 475 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/users/notes.ts') diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 204e1d0170..6fe0e05c6d 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; import { RelayService } from '@/core/RelayService.js'; diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index e6102a1b91..15084b8ff1 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -4,7 +4,7 @@ import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import * as nsfw from 'nsfwjs'; import si from 'systeminformation'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; const _filename = fileURLToPath(import.meta.url); diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index e0af033952..0146f959e6 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -10,7 +10,7 @@ import * as Acct from '@/misc/acct.js'; import { Cache } from '@/misc/cache.js'; import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; -import { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from './UtilityService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index b1b52fd6a9..67b4b90061 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { HttpRequestService } from './HttpRequestService.js'; type CaptchaResponse = { diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index 525fac6d92..feb82dcbf9 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 32dad70d1c..e1355fff07 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -2,14 +2,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { Cache } from '@/misc/cache.js'; import { query } from '@/misc/prelude/url.js'; import type { Note } from '@/models/entities/Note.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { UtilityService } from './UtilityService.js'; import { ReactionService } from './ReactionService.js'; diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index ba67bc499e..53d48c450b 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueueService } from '@/core/QueueService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 81939d5f51..25965b7ac4 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -7,7 +7,7 @@ import PrivateIp from 'private-ip'; import got, * as Got from 'got'; import chalk from 'chalk'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { createTemp } from '@/misc/create-temp.js'; import { StatusError } from '@/misc/status-error.js'; diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index e356fa0009..643c51c37d 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -4,8 +4,8 @@ import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import type { IRemoteUser, User } from '@/models/entities/User.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 521ab7fd81..4c5cf7dfc4 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -3,9 +3,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { LoggerService } from '@/core/LoggerService.js'; @Injectable() diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index a4894a4376..b98a41f757 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; import { Cache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 376617914e..9a51ed0c0a 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -4,7 +4,7 @@ import { JSDOM } from 'jsdom'; import fetch from 'node-fetch'; import tinycolor from 'tinycolor2'; import type { Instance } from '@/models/entities/Instance.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index c36de63fde..df0c9b5ccb 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -24,7 +24,7 @@ import type { } from '@/server/api/stream/types.js'; import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; @Injectable() export class GlobalEventService { diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index f6c06d48f4..83950aa890 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -5,7 +5,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; import type { Hashtag } from '@/models/entities/Hashtag.js'; import HashtagChart from '@/core/chart/charts/hashtag.js'; -import { HashtagsRepository, UsersRepository } from '@/models/index.js'; +import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from './entities/UserEntityService.js'; @Injectable() diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index f4c00cd259..396fefad1c 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -5,7 +5,7 @@ import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 345b72bac5..997be17937 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { genAid } from '@/misc/id/aid.js'; import { genMeid } from '@/misc/id/meid.js'; import { genMeidg } from '@/misc/id/meidg.js'; diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index d215be2131..3a50361a42 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; export type IImage = { data: Buffer; diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 57d55870b1..fa906df4a2 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { ILocalUser } from '@/models/entities/User.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 9bc3597baf..6d2a9b2db6 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 558e3016dc..a3192c0262 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as SyslogPro from 'syslog-pro'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import Logger from '@/logger.js'; @Injectable() diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts index 1819b32a45..0603da0651 100644 --- a/packages/backend/src/core/MessagingService.ts +++ b/packages/backend/src/core/MessagingService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import type { Note } from '@/models/entities/Note.js'; @@ -10,7 +10,7 @@ import type { UserGroup } from '@/models/entities/UserGroup.js'; import { QueueService } from '@/core/QueueService.js'; import { toArray } from '@/misc/prelude/array.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js'; import { IdService } from './IdService.js'; import { GlobalEventService } from './GlobalEventService.js'; import { UserEntityService } from './entities/UserEntityService.js'; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 236be4bbf8..2e03bf3cc0 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -4,7 +4,7 @@ import * as parse5 from 'parse5'; import { JSDOM } from 'jsdom'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 191148ac25..81ae322b95 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { ModerationLogsRepository } from '@/models/index.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 5acc07fba6..6e5cce8f62 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -6,7 +6,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; import { Note } from '@/models/entities/Note.js'; -import { ChannelFollowingsRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { App } from '@/models/entities/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -23,7 +23,7 @@ import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import NotesChart from '@/core/chart/charts/notes.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import InstanceChart from '@/core/chart/charts/instance.js'; diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 48d0d31e88..ccc583c5b6 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -2,11 +2,11 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js'; -import { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; +import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import NotesChart from '@/core/chart/charts/notes.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import InstanceChart from '@/core/chart/charts/instance.js'; diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index 576e90bd43..b70c051efd 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -1,13 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; import type { UserNotePining } from '@/models/entities/UserNotePining.js'; import { RelayService } from '@/core/RelayService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { UserEntityService } from './entities/UserEntityService.js'; import { ApDeliverManagerService } from './remote/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from './remote/activitypub/ApRendererService.js'; diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ca9e60889d..2606ca4de0 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { NotificationsRepository } from '@/models/index.js'; +import type { NotificationsRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 8bc94c8a82..1bb68f7804 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; import type { CacheableUser } from '@/models/entities/User.js'; diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 40ccc8226a..07d8d0dbd5 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +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 './MetaService.js'; diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 31d29bed97..5eaaed00eb 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,10 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import push from 'web-push'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { Packed } from '@/misc/schema'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { MetaService } from './MetaService.js'; // Defined also packages/sw/types.ts#L14-L21 diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 7e771c100f..12be57c7fb 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -3,9 +3,9 @@ import { v4 as uuid } from 'uuid'; import type { IActivity } from '@/core/remote/activitypub/type.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; -import { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './queue/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './queue/QueueModule.js'; import type { ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 3006456577..d5b3c0e799 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { IRemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 688ea03d34..5324826ec1 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { ILocalUser, User } from '@/models/entities/User.js'; -import { RelaysRepository, UsersRepository } from '@/models/index.js'; +import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Cache } from '@/misc/cache.js'; import type { Relay } from '@/models/entities/Relay.js'; diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 9549e19990..723a79dc59 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -2,7 +2,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import S3 from 'aws-sdk/clients/s3.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from './HttpRequestService.js'; diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index a876668b94..8b72d73af6 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UsedUsernamesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsedUsernamesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { User } from '@/models/entities/User.js'; import { UserProfile } from '@/models/entities/UserProfile.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts index be31534c02..0962f88a7c 100644 --- a/packages/backend/src/core/TwoFactorAuthenticationService.ts +++ b/packages/backend/src/core/TwoFactorAuthenticationService.ts @@ -2,8 +2,8 @@ import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import * as jsrsasign from 'jsrsasign'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; const ECC_PRELUDE = Buffer.from([0x04]); const NULL_BYTE = Buffer.from([0]); diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 8212abf7bb..666bef3f49 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import type { CacheableLocalUser, CacheableUser, ILocalUser } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts index e53f37b714..8eca03a10b 100644 --- a/packages/backend/src/core/UserKeypairStoreService.ts +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { User } from '@/models/entities/User.js'; -import { UserKeypairsRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 03113f042a..b1d01a1565 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { UserList } from '@/models/entities/UserList.js'; import type { UserListJoining } from '@/models/entities/UserListJoining.js'; diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 9146360df1..4c09e450c8 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, MutingsRepository } from '@/models/index.js'; +import type { UsersRepository, MutingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 068341cb2c..82c2e98236 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not, IsNull } from 'typeorm'; -import { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { ApRendererService } from './remote/activitypub/ApRendererService.js'; import { UserEntityService } from './entities/UserEntityService.js'; diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index ba03dfc069..15dd684286 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -2,7 +2,7 @@ import { URL } from 'node:url'; import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; @Injectable() export class UtilityService { diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 70b9664c76..af4036a291 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import FFmpeg from 'fluent-ffmpeg'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +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'; diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 1d74290dd9..347b2b16c4 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; -import { WebhooksRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index 4366d4cce1..21e4cedea3 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index be70bc79c0..2e0f4c7126 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index e1bfeabf99..2153cfe4b4 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import { NotesRepository } from '@/models/index.js'; +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'; 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 752203daaf..a44460bb4e 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; 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 48bf3d7c62..5ea08a0872 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -4,7 +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 { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-following.js'; 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 ffe52dcd56..5c14309d89 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -4,7 +4,7 @@ import type { User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-notes.js'; diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index b3187997cf..f0359968eb 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -4,7 +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 { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/users.js'; diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 6cc511fb48..1660894571 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { AbuseUserReportsRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 9193cb81d7..44110e7364 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { Antenna } from '@/models/entities/Antenna.js'; diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 6491b0b2d9..1cc7ca11dc 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { AccessTokensRepository, AppsRepository } from '@/models/index.js'; +import type { AccessTokensRepository, AppsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { App } from '@/models/entities/App.js'; diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index a4dab3d9cf..bf8efa5f78 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { AuthSessionsRepository } from '@/models/index.js'; +import type { AuthSessionsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { AuthSession } from '@/models/entities/AuthSession.js'; diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index 74ce6830b6..49a96037ca 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { BlockingsRepository } from '@/models/index.js'; +import type { BlockingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { Blocking } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index fec76e4e63..860967443e 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 27637e42e8..7a5d2f7f0a 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 521bf51da0..f0ac6518d0 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -2,8 +2,8 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { Packed } from '@/misc/schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { User } from '@/models/entities/User.js'; diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index beebbc3206..5761fa37bc 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 10ed0f19ee..fc09b5a2c7 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index f7e7fd42e4..4a60c1263f 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { FollowRequestsRepository } from '@/models/index.js'; +import type { FollowRequestsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index 93fed85f72..c7e040a57b 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index 9473ed90b7..bf7c2b7f8d 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { GalleryPosts, GalleryLikesRepository } from '@/models/index.js'; +import type { GalleryPosts, GalleryLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index 82b41697c9..ca98687d7b 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index 6dcbf49030..511992c44f 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index c58c2f8f34..c54285d9df 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/MessagingMessageEntityService.ts b/packages/backend/src/core/entities/MessagingMessageEntityService.ts index 04467b94e4..b7c42a5760 100644 --- a/packages/backend/src/core/entities/MessagingMessageEntityService.ts +++ b/packages/backend/src/core/entities/MessagingMessageEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MessagingMessagesRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index 45d15088fc..2f508710b8 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { ModerationLogsRepository } from '@/models/index.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index c5245bf203..862be009da 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 669680758d..a9c63566b4 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -4,7 +4,7 @@ import * as mfm from 'mfm-js'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Notes, Polls, PollVotes, DriveFiles, Channels, Followings, Users, NoteReactions } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { Packed } from '@/misc/schema.js'; import { nyaize } from '@/misc/nyaize.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index f0bbf27b6a..1a68a5c628 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index e64f2af681..47008ee08e 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { NoteReactionsRepository } from '@/models/index.js'; +import type { NoteReactionsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { OnModuleInit } from '@nestjs/common'; diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 6a0683d543..c415599fea 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js'; +import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Notification } from '@/models/entities/Notification.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index cbd193fe0a..004443759b 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index dccaf2edec..62d9c82ca6 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { PageLikesRepository } from '@/models/index.js'; +import type { PageLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts index 521c7669e7..fd89662f7d 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { SigninsRepository } from '@/models/index.js'; +import type { SigninsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 343e42df07..a35703e80e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -1,9 +1,9 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { EntityRepository, Repository, In, Not } from 'typeorm'; +import type { EntityRepository, Repository, In, Not } from 'typeorm'; import Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { Packed } from '@/misc/schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; diff --git a/packages/backend/src/core/entities/UserGroupEntityService.ts b/packages/backend/src/core/entities/UserGroupEntityService.ts index acd26ea1eb..e399197618 100644 --- a/packages/backend/src/core/entities/UserGroupEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js'; +import type { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts index 50ff2231ab..f5c9be3475 100644 --- a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { UserGroupInvitationsRepository } from '@/models/index.js'; +import type { UserGroupInvitationsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index 05434d9c8a..e2b0814914 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/remote/ResolveUserService.ts b/packages/backend/src/core/remote/ResolveUserService.ts index b45168fb02..2fd9e7c378 100644 --- a/packages/backend/src/core/remote/ResolveUserService.ts +++ b/packages/backend/src/core/remote/ResolveUserService.ts @@ -3,9 +3,9 @@ import { Inject, Injectable } from '@nestjs/common'; import chalk from 'chalk'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import type { IRemoteUser, User } from '@/models/entities/User.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { UtilityService } from '../UtilityService.js'; import { WebfingerService } from './WebfingerService.js'; diff --git a/packages/backend/src/core/remote/WebfingerService.ts b/packages/backend/src/core/remote/WebfingerService.ts index ab46314792..d2a88be583 100644 --- a/packages/backend/src/core/remote/WebfingerService.ts +++ b/packages/backend/src/core/remote/WebfingerService.ts @@ -1,7 +1,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { query as urlQuery } from '@/misc/prelude/url.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; diff --git a/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts b/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts index 6f197985da..77d200c3c8 100644 --- a/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/remote/activitypub/ApDbResolverService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; -import { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; import { Cache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; diff --git a/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts index a6ee857526..6fc75a0397 100644 --- a/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/remote/activitypub/ApDeliverManagerService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { FollowingsRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/core/remote/activitypub/ApInboxService.ts b/packages/backend/src/core/remote/activitypub/ApInboxService.ts index 0482e029d2..a3cb08063d 100644 --- a/packages/backend/src/core/remote/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/remote/activitypub/ApInboxService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { CacheableRemoteUser } from '@/models/entities/User.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ReactionService } from '@/core/ReactionService.js'; diff --git a/packages/backend/src/core/remote/activitypub/ApMfmService.ts b/packages/backend/src/core/remote/activitypub/ApMfmService.ts index 3c3b98b139..8804fde64a 100644 --- a/packages/backend/src/core/remote/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/remote/activitypub/ApMfmService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { MfmService } from '@/core/MfmService.js'; import type { Note } from '@/models/entities/Note.js'; import { extractApHashtagObjects } from './models/tag.js'; diff --git a/packages/backend/src/core/remote/activitypub/ApRendererService.ts b/packages/backend/src/core/remote/activitypub/ApRendererService.ts index 5a4cef63e0..6058929d35 100644 --- a/packages/backend/src/core/remote/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/remote/activitypub/ApRendererService.ts @@ -4,7 +4,7 @@ import { In, IsNull } from 'typeorm'; import { v4 as uuid } from 'uuid'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js'; import type { Blocking } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/remote/activitypub/ApRequestService.ts b/packages/backend/src/core/remote/activitypub/ApRequestService.ts index 2abaca06af..baad46d668 100644 --- a/packages/backend/src/core/remote/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/remote/activitypub/ApRequestService.ts @@ -2,7 +2,7 @@ import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +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'; diff --git a/packages/backend/src/core/remote/activitypub/ApResolverService.ts b/packages/backend/src/core/remote/activitypub/ApResolverService.ts index 9d8e177758..e2d6a77370 100644 --- a/packages/backend/src/core/remote/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/remote/activitypub/ApResolverService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import type { ILocalUser } from '@/models/entities/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; -import { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/remote/activitypub/models/ApImageService.ts b/packages/backend/src/core/remote/activitypub/models/ApImageService.ts index da6ed61c56..9bf87f19d4 100644 --- a/packages/backend/src/core/remote/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/remote/activitypub/models/ApImageService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { CacheableRemoteUser } from '@/models/entities/User.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts b/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts index 898da07a26..710e1acfaf 100644 --- a/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/remote/activitypub/models/ApMentionService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { toArray, unique } from '@/misc/prelude/array.js'; import type { CacheableUser } from '@/models/entities/User.js'; import { isMention } from '../type.js'; diff --git a/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts b/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts index 1efe62333b..a34a1d1eb8 100644 --- a/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/remote/activitypub/models/ApNoteService.ts @@ -1,9 +1,9 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type { CacheableRemoteUser } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; diff --git a/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts b/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts index d088fa5554..5135473862 100644 --- a/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/remote/activitypub/models/ApPersonService.ts @@ -3,8 +3,8 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { CacheableUser, IRemoteUser } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js'; import { truncate } from '@/misc/truncate.js'; diff --git a/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts index 2b89cb030b..acd5cdae83 100644 --- a/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/remote/activitypub/models/ApQuestionService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, PollsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { NotesRepository, PollsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type { IPoll } from '@/models/entities/Poll.js'; import type Logger from '@/logger.js'; import { isQuestion } from '../type.js'; diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts index a51f570725..dbad576abe 100644 --- a/packages/backend/src/daemons/JanitorService.ts +++ b/packages/backend/src/daemons/JanitorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { AttestationChallengesRepository } from '@/models/index.js'; +import type { AttestationChallengesRepository } from '@/models/index.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const interval = 30 * 60 * 1000; diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts index fcc9873a6f..7622ab8800 100644 --- a/packages/backend/src/queue/DbQueueProcessorsService.ts +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { DbJobData } from '@/queue/types.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; diff --git a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts index 402c038be0..659e9b8e42 100644 --- a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts +++ b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { ObjectStorageJobData } from '@/queue/types.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; import type Bull from 'bull'; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 753df8cad6..a4879cef3d 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/queue/SystemQueueProcessorsService.ts b/packages/backend/src/queue/SystemQueueProcessorsService.ts index 7c227296e7..ccb040fae5 100644 --- a/packages/backend/src/queue/SystemQueueProcessorsService.ts +++ b/packages/backend/src/queue/SystemQueueProcessorsService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 17337837a3..e91cba9d10 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { MutingsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { MutingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 6f2fb8dea0..e8e90f1422 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 830f0c56b6..6eb457ce9f 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UserIpsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UserIpsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index c3c68be1bc..4d46cdeaf9 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index ab82f87d5e..4d6ed2008a 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, UserProfilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository, UserProfilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 430fbf19e9..682382b2db 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UsersRepository, DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index 72923b80a9..6740643fe2 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 1bf51c1bc6..9042a21d2c 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/remote/activitypub/ApRequestService.js'; diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 3e55a351a1..2fc7fe219e 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { PollVotesRepository, NotesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index cbc483698f..db149b68cf 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -3,9 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import { UsersRepository, BlockingsRepository } from '@/models/index.js'; -import type { DriveFilesRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository, BlockingsRepository, DriveFilesRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index c49a47561b..aa7a0ef929 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -6,8 +6,8 @@ import { ulid } from 'ulid'; import mime from 'mime-types'; import archiver from 'archiver'; import { DI } from '@/di-symbols.js'; -import { EmojisRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { EmojisRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 4c6162432a..eac9c6925e 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan, Not } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import { FollowingsRepository, MutingsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { FollowingsRepository, MutingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 7781d2787f..e263c245fd 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 62b3a53c49..533d4bd7c6 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 097835ac81..8c3e3dbe17 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 44c8800a68..12f77638bb 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { BlockingsRepository, DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { BlockingsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 4919fb2f7b..492f17f9ff 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan, DataSource } from 'typeorm'; import unzipper from 'unzipper'; import { DI } from '@/di-symbols.js'; -import { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { createTempDir } from '@/misc/create-temp.js'; diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 5e49678d05..f649014399 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index c613c7e74e..f004f2d64b 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 96c862e5c9..168f850718 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { ResolveUserService } from '@/core/remote/ResolveUserService.js'; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 4593b4fb61..ad72263a8d 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import httpSignature from '@peertube/http-signature'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/remote/activitypub/ApRequestService.js'; diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 75d02d527a..bf2fdeb7a0 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index e16956df0c..96607e1d68 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 27243be51b..43e3f37201 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { WebhooksRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { WebhooksRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 21ecc7177a..b2fcf9f81a 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -4,9 +4,9 @@ import json from 'koa-json-body'; import httpSignature from '@peertube/http-signature'; import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; +import type { EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; import * as url from '@/misc/prelude/url.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; import { QueueService } from '@/core/QueueService.js'; import type { ILocalUser, User } from '@/models/entities/User.js'; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index becf0592d7..dc073e34ac 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -7,8 +7,8 @@ import cors from '@koa/cors'; import Router from '@koa/router'; import send from 'koa-send'; import rename from 'rename'; -import { Config } from '@/config.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { createTemp } from '@/misc/create-temp.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts index 1e0385602c..31841d39df 100644 --- a/packages/backend/src/server/MediaProxyServerService.ts +++ b/packages/backend/src/server/MediaProxyServerService.ts @@ -5,7 +5,7 @@ import cors from '@koa/cors'; import Router from '@koa/router'; import sharp from 'sharp'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { createTemp } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 04a5f1484b..b07dc4cf8f 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import Router from '@koa/router'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Cache } from '@/misc/cache.js'; diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 14d5ed45ab..d42972614f 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -9,8 +9,8 @@ import koaLogger from 'koa-logger'; import * as slow from 'koa-slow'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { Config } from '@/config.js'; -import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { envOption } from '@/env.js'; diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 7f827d439b..f2eee88e09 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import Router from '@koa/router'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; import type { User } from '@/models/entities/User.js'; import * as Acct from '@/misc/acct.js'; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index d13b8d5ced..c3ce12e0c3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -5,7 +5,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import type { CacheableLocalUser, User } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type Logger from '@/logger.js'; -import { UserIpsRepository } from '@/models/index.js'; +import type { UserIpsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index cfe39238dd..52654dbaee 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -5,8 +5,8 @@ import multer from '@koa/multer'; import bodyParser from 'koa-bodyparser'; import cors from '@koa/cors'; import { ModuleRef } from '@nestjs/core'; -import { Config } from '@/config.js'; -import { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import endpoints from './endpoints.js'; diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 29d6ba78f0..4ce9b91f42 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; +import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { CacheableLocalUser, ILocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { Cache } from '@/misc/cache.js'; diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 5cda3c6205..a5e2b09012 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -4,8 +4,8 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { ILocalUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 19d14bad67..3b96dfee6f 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { SigninsRepository } from '@/models/index.js'; +import type { SigninsRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; import type { ILocalUser } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index df040ddcf8..06b8b29134 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; import bcrypt from 'bcryptjs'; import { DI } from '@/di-symbols.js'; -import { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index b08b01aef9..8a6906e5fe 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -3,8 +3,8 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import * as websocket from 'websocket'; import { DI } from '@/di-symbols.js'; -import { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; diff --git a/packages/backend/src/server/api/common/GetterService.ts b/packages/backend/src/server/api/common/GetterService.ts index 5523539b91..a6b60d1f5a 100644 --- a/packages/backend/src/server/api/common/GetterService.ts +++ b/packages/backend/src/server/api/common/GetterService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 480ae7166e..30183ed88b 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AbuseUserReportsRepository } from '@/models/index.js'; +import type { AbuseUserReportsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; 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 1b173379a0..c76ece9e05 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { localUsernameSchema, passwordSchema } from '@/models/entities/User.js'; 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 2e0222f0c6..dc2d499191 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 6b32391e8d..8fcbde591b 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 7abefe156b..f4c9885408 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index efece31bbf..29e245ab95 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 098a593379..195300666e 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index ee07170d62..751b6be7f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 9a67bdb1aa..18d50b8b2a 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 35c14abda2..9b20494129 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; import type { Announcement } from '@/models/entities/Announcement.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 38358dff10..2393c2441c 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index c8b67fe1c0..d0485fddd8 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; 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 051e4c60fb..22b78bf19d 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 @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; 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 index 770bade06d..f4d39cd872 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index 3927a89f90..4f7e02fe92 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -1,7 +1,7 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 88529ab0aa..2459a479ab 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; 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 45ea9cdb50..c17f67cf2b 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 @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 0b6e744ef8..7c24e8baa8 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index daa57e8eb2..c4e1987d73 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository, EmojisRepository } from '@/models/index.js'; +import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 08d40834c1..2cdd9c36bd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 81b095cb57..8b2031e6dd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index e4278dc33a..dd7cd4cede 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 9d6fa53417..c03d27878c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 736d664cc3..e50f924044 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index d6c70eaae7..99512a26b3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index c438b7f9b7..697999cc7c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 4a9b31fd28..00a5b162bf 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index e6eb9eb9a6..c576950ac7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index 789838661c..38fe99b222 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 476b821523..b7f2858a77 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 67165dc47e..b073209a5b 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index b9eade5b40..0a529ecb08 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; 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 eddaade919..947a673def 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 @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserIpsRepository } from '@/models/index.js'; +import type { UserIpsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts index 5fe341e5ca..bc42bf792a 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite.ts @@ -1,7 +1,7 @@ import rndstr from 'rndstr'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistrationTicketsRepository } from '@/models/index.js'; +import type { RegistrationTicketsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 615c0a0e70..5b43c180d8 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts index fe200da6ad..2fc5a35e8e 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts index 3dc7158ba9..f0d7a3f12d 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index a179f163df..0cff6bae6a 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { PromoNotesRepository } from '@/models/index.js'; +import type { PromoNotesRepository } from '@/models/index.js'; import { GetterService } from '@/server/api/common/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; 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 7446746b45..f7d27be9cb 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import rndstr from 'rndstr'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index b5828ae9be..a6e59276fb 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { QueueService } from '@/core/QueueService.js'; import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 2424cac425..ac0a84128c 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogsRepository } from '@/models/index.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; 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 b50564210b..e4031cf960 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; 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 8d11e3ea7a..d28f9c71b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index bec8f7719e..b9dbd211e0 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; 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 d9266aac6c..d30facd125 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index b4671a2f41..3a9d410de0 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.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'; diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 96283d251f..2805c21a74 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index 1ea0e6aac4..33808ee70f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index aa44dfd5dc..74168481f6 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models'; export const meta = { tags: ['meta'], diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 56bd343d55..fe24a10300 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { UserListsRepository, UserGroupJoiningsRepository, AntennasRepository } from '@/models/index.js'; +import type { UserListsRepository, UserGroupJoiningsRepository, AntennasRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 127aca0c33..5da7a2cb66 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index bdc44895cc..a0f8979574 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/index.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index eba42afe56..fb3c713154 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotesRepository, AntennaNotesRepository } from '@/models/index.js'; +import type { NotesRepository, AntennaNotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index 8bd8ad124d..ef7ed5b72c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/index.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 59bba04ee1..1955eac949 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AntennasRepository, UserListsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { AntennasRepository, UserListsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index e291b5908a..0a5fc31751 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, NotesRepository } from '@/models/index.js'; +import type { UsersRepository, NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import type { CacheableLocalUser, User } from '@/models/entities/User.js'; import { isActor, isPost, getApId } from '@/core/remote/activitypub/type.js'; diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index f52d18f7fe..c1d0a9dd74 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { unique } from '@/misc/prelude/array.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index f94fed5344..eaafa8dc1b 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 6032b59bef..cb2e661bfb 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -1,7 +1,7 @@ import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/index.js'; +import type { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index 7f8325dbbd..6108d8202d 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -1,9 +1,9 @@ import { v4 as uuid } from 'uuid'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; +import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index dff4c74340..db3bf7aa63 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AuthSessionsRepository } from '@/models/index.js'; +import type { AuthSessionsRepository } from '@/models/index.js'; import { AuthSessionEntityService } from '@/core/entities/AuthSessionEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index 9c9f13f502..b1e7bbfded 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/index.js'; +import type { UsersRepository, AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 33614a1554..aa6d2ecf20 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index f2cc28e922..46a499943c 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 4f5e11cd68..969aae06f9 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { BlockingsRepository } from '@/models/index.js'; +import type { BlockingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 21979884f9..10f8b24629 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import type { Channel } from '@/models/entities/Channel.js'; import { IdService } from '@/core/IdService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index 0c3f9509d1..d25faae38d 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 6c6b498a94..871d3927bc 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index 5a8ab26af9..f49f3105d5 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelFollowingsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index 8b8b5819e6..59df0616be 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 54ae31790b..8718615db2 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 1c7f1360b9..58f8835279 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelsRepository, NotesRepository } from '@/models/index.js'; +import type { ChannelsRepository, NotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index b464c55097..ac2ef825be 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index ba62e9d371..d006e89bd2 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index c733d28657..77d02815e0 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; -import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { ApiError } from '../../error.js'; import { GetterService } from '../../common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index 8eca3d66d1..d300203a21 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index ea361ae9c0..077a9ec40f 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index b57affd1c4..63ca069364 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 4282498931..6818d31cc4 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/index.js'; +import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 3fc60e3639..93805af089 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; import { GetterService } from '../../common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 4e93540054..e6d3f4f1f8 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 9880505d06..597b67c442 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 56055d1340..f6fad50fd9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 9f11eb8b53..328d0e4643 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index 176031d808..290cd4d2ce 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index bff83876d7..d394f5c3da 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; 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 9d2ea6011a..be7b050907 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 6299ca8f6b..d6d85f4e77 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index e4cd5213dd..858063eb4b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; 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 bae4d7d66f..474d599cb6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { DriveFile } from '@/models/entities/DriveFile.js'; -import { DriveFilesRepository } from '@/models/index.js'; +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'; 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 03e3663f08..703f92d8c6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index f4f8df3c2b..19ab03a337 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 703dc83ecd..b41eaf4463 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 7604eaf489..e7c11a8c13 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index dcbaecf8af..d921bc1b17 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index 96a87344a9..ee24db11f2 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index 4c25bc705c..c06263b902 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 4fcd37bbbf..ee63d291b2 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index aba73c2092..61bcfea0c3 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index e5222fcbfd..be1d6c8e58 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index a20c5a31b3..74656ce863 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index e7f8cefff5..81276a7ab0 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index f855b54537..66502748b3 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index d07a08637f..19418e698c 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -1,6 +1,6 @@ import { IsNull, MoreThan, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 0400cacd02..a028930f21 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 9e6a3cc717..58fa01ac48 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -1,7 +1,7 @@ import Parser from 'rss-parser'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 3a06c63d52..be322e2896 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 07366bc821..afb59dd2c2 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 8285189d66..e67e136ead 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 0b79a80649..213b5ce32e 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index c36d4a077f..5b11633e6f 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { FollowRequestsRepository } from '@/models/index.js'; +import type { FollowRequestsRepository } from '@/models/index.js'; import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 3b892ef522..9994ce90d7 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 551ea64835..55d3dabfb0 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index 4afcbce816..e94003eb79 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 9e8bcac131..2842308510 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; import { GalleryPost } from '@/models/entities/GalleryPost.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index ad5f95c853..6cdcc17b39 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 8aca98119b..519e56ed6a 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index 723906d60f..f7e828142b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index d878582998..cfbedcc4d9 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository, GalleryLikesRepository } from '@/models/index.js'; +import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 1900afaeb6..d261aaa966 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; import { GalleryPost } from '@/models/entities/GalleryPost.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index 2d9bf29dd9..dea0f4799c 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,7 +1,7 @@ import { MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index a7e7e6ba35..226a11de0b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 3fb77bef9b..7f787ea38f 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 59170f6d0e..06b0d6e9b2 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 7e483ea214..cf45cc6c24 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,7 +1,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 10a88fbefa..c3f2ea9ea7 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 815b3168b4..3bcd6ff8fb 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index bcf3931b04..ec9ac1ef90 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,7 +1,7 @@ import * as speakeasy from 'speakeasy'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index f2f4c2044e..6e0849f2b2 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -4,11 +4,11 @@ import * as cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; -import { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; const cborDecodeFirst = promisify(cbor.decodeFirst) as any; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index 3eb9f43c2b..0655a86350 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index df37db4c6a..19c77365c6 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -3,7 +3,7 @@ import * as crypto from 'node:crypto'; import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; +import type { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index e20911f35e..a539c5c221 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -2,10 +2,10 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; export const meta = { requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 1889dd7893..f40ec9797d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 4607e5d981..4c5b151f78 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 8d5851659b..3361e5a4d3 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index a5592d20de..d0bdb5695b 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index cc5b712ecf..873835a36c 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index a1804599df..77a03d9811 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 350abd9f7b..ce8ab4962a 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteFavoriteEntityService } from '@/core/entities/NoteFavoriteEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index ff6bcc01ab..d1b04cb655 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryLikesRepository } from '@/models/index.js'; +import type { GalleryLikesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryLikeEntityService } from '@/core/entities/GalleryLikeEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 927be51f79..32d14293f7 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts index 0695abdd85..3179457817 100644 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MutedNotesRepository } from '@/models/index.js'; +import type { MutedNotesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index bfba1fc36f..8c1c158ab1 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index c7cb2e0330..383bdc02b5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 060c37c13f..345ad916cb 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index a5e17283e5..875af7ec23 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 96927dad49..13de3382dd 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; import { notificationTypes } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index 9a909eedf4..70e6e0a6a8 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { PageLikesRepository } from '@/models/index.js'; +import type { PageLikesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { PageLikeEntityService } from '@/core/entities/PageLikeEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 7c4e4a6c7d..285aa34e91 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts index 36c3566f55..109d6d1068 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index b4bb83c6eb..b92de4b739 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteUnreadsRepository } from '@/models/index.js'; +import type { NoteUnreadsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 5a7909674f..cb5b4b0a60 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 7796fd97cb..f942f43cc8 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 3b4db5fae3..17154c1f76 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index d24dff95b0..233686dbe1 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 98d94a4c02..99cdf95bad 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index d1a05d9d06..362a5e89f4 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 6df5f4ecc3..99f69d8bed 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index b5870f099d..78a641f5e2 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index 58085ddbc5..0a4ecb9c51 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 585aac2e01..c8e72203c4 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 86a82e6a6c..5e1dddb6b7 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 410cd72065..9b30a24336 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SigninsRepository } from '@/models/index.js'; +import type { SigninsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 719cc14f09..b656c5c51d 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -3,10 +3,10 @@ import rndstr from 'rndstr'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 4b904d4696..9bf0616e3a 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -3,7 +3,7 @@ import * as mfm from 'mfm-js'; import { Inject, Injectable } from '@nestjs/common'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; +import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts index 6dd1626bb8..1ad2f7d68f 100644 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupInvitationsRepository } from '@/models/index.js'; +import type { UserGroupInvitationsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UserGroupInvitationEntityService } from '@/core/entities/UserGroupInvitationEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 016b1b5d6a..584c2ba6a4 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { webhookEventTypes } from '@/models/entities/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 53b553b43e..7bdad136aa 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 8e4aff45dd..58c84938cc 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 622c2ade98..d15ca0050d 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 3a0ef1a526..50098f96e7 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { webhookEventTypes } from '@/models/entities/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts index da3ba59df9..0b6099d4ac 100644 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ b/packages/backend/src/server/api/endpoints/messaging/history.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; -import { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js'; +import type { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js'; import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index 6579b03987..f563da3278 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/index.js'; -import { UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index e02afcbcfd..f61662af75 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; +import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import { GetterService } from '@/server/api/common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts index 5baecb9114..cd74f5f197 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MessagingMessagesRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository } from '@/models/index.js'; import { MessagingService } from '@/core/MessagingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts index 6e66cafe1e..bddb6d932d 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MessagingMessagesRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository } from '@/models/index.js'; import { MessagingService } from '@/core/MessagingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 9a6258d7dd..5c09c33941 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,13 +1,13 @@ import { IsNull, MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { AdsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; +import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { MetaService } from '@/core/MetaService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index d8eb89c0e6..97def86262 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index cbdd001185..3b4507281f 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/index.js'; import type { Muting } from '@/models/entities/Muting.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index c7098059d5..2fc5cee95e 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 11c05eb795..9ec6d17273 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { MutingEntityService } from '@/core/entities/MutingEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index 90cd53a133..4b7ed80123 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 288e195316..0a8f2292ac 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 86f90e049f..ea7a825f9d 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 7d893f32a1..579466d4fd 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,6 +1,6 @@ import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 2f8324ed62..ddfee31525 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { Note } from '@/models/entities/Note.js'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 30b7a889fc..92bc8a7595 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -2,7 +2,7 @@ import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { User } from '@/models/entities/User.js'; -import { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Note } from '@/models/entities/Note.js'; import type { Channel } from '@/models/entities/Channel.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 4769c8bdf1..5765dfe66f 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index bfdd1acd22..edbd300e08 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 6b3a02b101..8f4f2b2b9e 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; import { DI } from '@/di-symbols.js'; -import { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { ApiError } from '../../../error.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 9985f9d257..76834cfde9 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; 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 73b5afa40a..b6eaccb5ac 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; 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 c6458223eb..6573e9454a 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; 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 7b8859639d..fac14fa225 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 9b2dabc88b..92b82eb5de 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 11bfdbba0f..6cdc9b902c 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,6 +1,6 @@ import { Brackets, In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 76f07528d7..515f03dcce 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,6 +1,6 @@ import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { IRemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index d57950f012..02ae212a30 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,6 +1,6 @@ import { DeepPartial } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NoteReactionsRepository } from '@/models/index.js'; +import type { NoteReactionsRepository } from '@/models/index.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 57b7aeae0d..97ef1a17ec 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 7020d0c681..4df95962c8 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 0727c9af6c..061e371d65 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 484cfc1128..27b477e141 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,10 +1,10 @@ import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index c3f5b9dfb0..7849cfa401 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 7756d39f7c..a02b8d2559 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js'; +import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index 060581d74b..b8ddf83f3c 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; +import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index aed15852d4..54e9b4939f 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NoteThreadMutingsRepository } from '@/models/index.js'; +import type { NoteThreadMutingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 53a1ae1348..8542af17db 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index c24f1e401e..7a3daf741e 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,9 +1,9 @@ import { URLSearchParams } from 'node:url'; import fetch from 'node-fetch'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index c0048888b4..7378c4b600 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, NotesRepository } from '@/models/index.js'; +import type { UsersRepository, NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 87a464578c..9b23103fd4 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 3d1eb2b39c..09134cf48f 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { NotificationsRepository } from '@/models/index.js'; +import type { NotificationsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 1b0299c3c6..1841a84539 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index ac80849aa0..eae8f18403 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { DriveFilesRepository, PagesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, PagesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Page } from '@/models/entities/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index 4e97755761..e64733131c 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index 3e3dbb0832..31844165e2 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index f3c55fed8b..41a11d1a31 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 6d73889d39..651252afbb 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,6 +1,6 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, PagesRepository } from '@/models/index.js'; +import type { UsersRepository, PagesRepository } from '@/models/index.js'; import type { Page } from '@/models/entities/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 88386739be..e397e2a23b 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 8980ac4906..4db0f80b26 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { PagesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { PagesRepository, DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 573331e0d8..6c941314e2 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,6 +1,6 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 7c8188ce3c..a28229add3 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { PromoReadsRepository } from '@/models/index.js'; +import type { PromoReadsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 4766239533..42b10a4fb3 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -2,10 +2,10 @@ import rndstr from 'rndstr'; import ms from 'ms'; import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { EmailService } from '@/core/EmailService.js'; import { ApiError } from '../error.js'; diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 48edde5196..cf7fcb7afd 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; +import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 17af75578b..3adf0a4bb8 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; +import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 73a084c2ad..ddec877dd4 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; -import { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index feb6730154..5772eeee26 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 56474d6988..c80b6efdcd 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,6 +1,6 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; +import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { localUsernameSchema } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 3d05ec2e1d..b015129a7a 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 2d5545cbab..e3fd0920c9 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 08bcdd9f88..17ce920011 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,6 +1,6 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 225ab5210a..6dbda0d72f 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,6 +1,6 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 2d28d6ca07..6e57eee5fb 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 3eeca7562f..22c49860bd 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,7 +1,7 @@ import { Not, In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { maximum } from '@/misc/prelude/array.js'; -import { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index 5d7ad84ae0..c1f4f48445 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index 50156b049e..d238ae9f16 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository } from '@/models/index.js'; +import type { UserGroupsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 0490fd41a0..f154a57f61 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupInvitationsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupInvitationsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 26efc1ecf3..1fd3b2f4b3 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupInvitationsRepository } from '@/models/index.js'; +import type { UserGroupInvitationsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 4ae32a6bda..127f4ca65b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvitationsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvitationsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index e7e69f257d..8daee3a6f5 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -1,6 +1,6 @@ import { Not, In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 0a63dbb7f1..846f80e64d 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index c9ae39561f..0bc6e8b3fc 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository } from '@/models/index.js'; +import type { UserGroupsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index e6f60eef0a..3474f22c6e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 1cebfcd204..2b0f403f33 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index a8b2533b73..e0b2b13c7f 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; import { GetterService } from '@/server/api/common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index b679625c85..5af849de14 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupsRepository } from '@/models/index.js'; +import type { UserGroupsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index aa64ca1229..99f0751ea8 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { UserList } from '@/models/entities/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index 0f4125a39f..237cb075ab 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 919de22377..2104c4377d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 89d97be93e..7e54d33376 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/common/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 77ad772b13..06ea43d654 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/common/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 62e730b2f7..77f9cba808 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index c6669d24d1..6453d7d980 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index bb8104584c..d2c9616f68 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index 96c7ef1e70..e007aa57b2 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { PagesRepository } from '@/models'; +import type { PagesRepository } from '@/models'; import { DI } from '@/di-symbols.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 6b4d882b7c..9ec911f322 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserProfilesRepository, NoteReactionsRepository } from '@/models/index.js'; +import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index e50a5706d9..5498b8c854 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,6 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index aea75ae799..ac9104bf92 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; 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 5c211a9017..bb37dd2715 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,6 +1,6 @@ import * as sanitizeHtml from 'sanitize-html'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 1747dc93f6..f13df3ee9d 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { USER_ACTIVE_THRESHOLD } from '@/const.js'; import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 9879b1b68b..ba07714972 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 98f5f03063..1e15025bf4 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,6 +1,6 @@ import { In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/integration/DiscordServerService.ts b/packages/backend/src/server/api/integration/DiscordServerService.ts index ea044c27d5..cc3f4e0633 100644 --- a/packages/backend/src/server/api/integration/DiscordServerService.ts +++ b/packages/backend/src/server/api/integration/DiscordServerService.ts @@ -4,8 +4,8 @@ import Router from '@koa/router'; import { OAuth2 } from 'oauth'; import { v4 as uuid } from 'uuid'; import { IsNull } from 'typeorm'; -import { Config } from '@/config.js'; -import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { ILocalUser } from '@/models/entities/User.js'; diff --git a/packages/backend/src/server/api/integration/GithubServerService.ts b/packages/backend/src/server/api/integration/GithubServerService.ts index 58b170d0e4..2195e82813 100644 --- a/packages/backend/src/server/api/integration/GithubServerService.ts +++ b/packages/backend/src/server/api/integration/GithubServerService.ts @@ -4,8 +4,8 @@ import Router from '@koa/router'; import { OAuth2 } from 'oauth'; import { v4 as uuid } from 'uuid'; import { IsNull } from 'typeorm'; -import { Config } from '@/config.js'; -import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { ILocalUser } from '@/models/entities/User.js'; diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts index a4a67f6c8c..7f209ea285 100644 --- a/packages/backend/src/server/api/integration/TwitterServerService.ts +++ b/packages/backend/src/server/api/integration/TwitterServerService.ts @@ -4,8 +4,8 @@ import Router from '@koa/router'; import { v4 as uuid } from 'uuid'; import { IsNull } from 'typeorm'; import autwh from 'autwh'; -import { Config } from '@/config.js'; -import { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { ILocalUser } from '@/models/entities/User.js'; diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index 5bf20c4101..b6ce6c217e 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js'; +import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js'; import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import { MessagingService } from '@/core/MessagingService.js'; 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 a45c7d9468..f9f0d02558 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { NotesRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 85b31312b3..44acd12796 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -13,7 +13,7 @@ import { createBullBoard } from '@bull-board/api'; import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { KoaAdapter } from '@bull-board/koa'; import { In, IsNull } from 'typeorm'; -import { Config } from '@/config.js'; +import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 8b676aebe5..1d7d49961d 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, IsNull } from 'typeorm'; import { Feed } from 'feed'; import { DI } from '@/di-symbols.js'; -import { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 1cbb3f36c2..f5dddd2db7 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import summaly from 'summaly'; import { DI } from '@/di-symbols.js'; -import { UsersRepository } from '@/models/index.js'; -import { Config } from '@/config.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 2952b8ba16..6d38a9fb9f 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -73,7 +73,7 @@ module.exports = { '@typescript-eslint/no-misused-promises': ['error', { 'checksVoidReturn': false, }], - '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/consistent-type-imports': 'off', '@typescript-eslint/prefer-nullish-coalescing': [ 'error', ], -- cgit v1.2.3-freya From 614b11951b6116a6485bee85126858189ecaa681 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 24 Sep 2022 07:15:16 +0900 Subject: refactor --- packages/backend/src/server/ServerModule.ts | 2 +- packages/backend/src/server/api/EndpointsModule.ts | 2 +- packages/backend/src/server/api/GetterService.ts | 74 ++++++++++++++++++++++ .../backend/src/server/api/common/GetterService.ts | 74 ---------------------- .../src/server/api/endpoints/admin/promo/create.ts | 2 +- .../src/server/api/endpoints/blocking/create.ts | 2 +- .../src/server/api/endpoints/blocking/delete.ts | 2 +- .../src/server/api/endpoints/clips/add-note.ts | 2 +- .../src/server/api/endpoints/clips/remove-note.ts | 2 +- .../api/endpoints/federation/update-remote-user.ts | 2 +- .../src/server/api/endpoints/following/create.ts | 2 +- .../src/server/api/endpoints/following/delete.ts | 2 +- .../server/api/endpoints/following/invalidate.ts | 2 +- .../api/endpoints/following/requests/accept.ts | 2 +- .../api/endpoints/following/requests/cancel.ts | 5 +- .../api/endpoints/following/requests/reject.ts | 2 +- .../src/server/api/endpoints/messaging/messages.ts | 5 +- .../api/endpoints/messaging/messages/create.ts | 2 +- .../src/server/api/endpoints/mute/create.ts | 2 +- .../src/server/api/endpoints/mute/delete.ts | 2 +- .../src/server/api/endpoints/notes/clips.ts | 2 +- .../src/server/api/endpoints/notes/conversation.ts | 2 +- .../src/server/api/endpoints/notes/delete.ts | 2 +- .../server/api/endpoints/notes/favorites/create.ts | 2 +- .../server/api/endpoints/notes/favorites/delete.ts | 2 +- .../src/server/api/endpoints/notes/polls/vote.ts | 2 +- .../server/api/endpoints/notes/reactions/create.ts | 2 +- .../server/api/endpoints/notes/reactions/delete.ts | 2 +- .../src/server/api/endpoints/notes/renotes.ts | 2 +- .../backend/src/server/api/endpoints/notes/show.ts | 2 +- .../api/endpoints/notes/thread-muting/create.ts | 2 +- .../api/endpoints/notes/thread-muting/delete.ts | 2 +- .../src/server/api/endpoints/notes/translate.ts | 2 +- .../src/server/api/endpoints/notes/unrenote.ts | 2 +- .../backend/src/server/api/endpoints/promo/read.ts | 2 +- .../users/get-frequently-replied-users.ts | 2 +- .../server/api/endpoints/users/groups/invite.ts | 2 +- .../src/server/api/endpoints/users/groups/pull.ts | 2 +- .../server/api/endpoints/users/groups/transfer.ts | 2 +- .../src/server/api/endpoints/users/lists/pull.ts | 2 +- .../src/server/api/endpoints/users/lists/push.ts | 2 +- .../src/server/api/endpoints/users/notes.ts | 2 +- .../src/server/api/endpoints/users/report-abuse.ts | 2 +- 43 files changed, 117 insertions(+), 119 deletions(-) create mode 100644 packages/backend/src/server/api/GetterService.ts delete mode 100644 packages/backend/src/server/api/common/GetterService.ts (limited to 'packages/backend/src/server/api/endpoints/users/notes.ts') diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index f05eda1cb8..474edafe41 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -7,7 +7,7 @@ import { MediaProxyServerService } from './MediaProxyServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ServerService } from './ServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; -import { GetterService } from './api/common/GetterService.js'; +import { GetterService } from './api/GetterService.js'; import { DiscordServerService } from './api/integration/DiscordServerService.js'; import { GithubServerService } from './api/integration/GithubServerService.js'; import { TwitterServerService } from './api/integration/TwitterServerService.js'; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index d2dfd78fd4..e41ed388b4 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -313,7 +313,7 @@ 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 { GetterService } from './common/GetterService.js'; +import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts new file mode 100644 index 0000000000..70ab46ec35 --- /dev/null +++ b/packages/backend/src/server/api/GetterService.ts @@ -0,0 +1,74 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; +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'; + +@Injectable() +export class GetterService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, + ) { + } + + /** + * Get note for API processing + */ + public async getNote(noteId: Note['id']) { + const note = await this.notesRepository.findOneBy({ id: noteId }); + + if (note == null) { + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + } + + return note; + } + + /** + * Get user for API processing + */ + public async getUser(userId: User['id']) { + const user = await this.usersRepository.findOneBy({ id: userId }); + + if (user == null) { + throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); + } + + return user; + } + + /** + * Get remote user for API processing + */ + public async getRemoteUser(userId: User['id']) { + const user = await this.getUser(userId); + + if (!this.userEntityService.isRemoteUser(user)) { + throw new Error('user is not a remote user'); + } + + return user; + } + + /** + * Get local user for API processing + */ + public async getLocalUser(userId: User['id']) { + const user = await this.getUser(userId); + + if (!this.userEntityService.isLocalUser(user)) { + throw new Error('user is not a local user'); + } + + return user; + } +} + diff --git a/packages/backend/src/server/api/common/GetterService.ts b/packages/backend/src/server/api/common/GetterService.ts deleted file mode 100644 index 70ab46ec35..0000000000 --- a/packages/backend/src/server/api/common/GetterService.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; -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'; - -@Injectable() -export class GetterService { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private userEntityService: UserEntityService, - ) { - } - - /** - * Get note for API processing - */ - public async getNote(noteId: Note['id']) { - const note = await this.notesRepository.findOneBy({ id: noteId }); - - if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); - } - - return note; - } - - /** - * Get user for API processing - */ - public async getUser(userId: User['id']) { - const user = await this.usersRepository.findOneBy({ id: userId }); - - if (user == null) { - throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); - } - - return user; - } - - /** - * Get remote user for API processing - */ - public async getRemoteUser(userId: User['id']) { - const user = await this.getUser(userId); - - if (!this.userEntityService.isRemoteUser(user)) { - throw new Error('user is not a remote user'); - } - - return user; - } - - /** - * Get local user for API processing - */ - public async getLocalUser(userId: User['id']) { - const user = await this.getUser(userId); - - if (!this.userEntityService.isLocalUser(user)) { - throw new Error('user is not a local user'); - } - - return user; - } -} - diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 0cff6bae6a..bee1ffbaee 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { PromoNotesRepository } from '@/models/index.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index aa6d2ecf20..c468010bce 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -6,7 +6,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index 46a499943c..46dd26a45a 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -6,7 +6,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index 77d02815e0..a242124e6a 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -4,7 +4,7 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 93805af089..55778c7ecb 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -3,7 +3,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index 57497bbf62..30e77aab45 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApPersonService } from '@/core/remote/activitypub/models/ApPersonService.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index be322e2896..f879429372 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -7,7 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['following', 'users'], diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index afb59dd2c2..4f12db1273 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -6,7 +6,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['following', 'users'], diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index e67e136ead..22304cacda 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -6,7 +6,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['following', 'users'], diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index 5f082c087a..dcb98485da 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 213b5ce32e..f39c4e3767 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,10 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 663c659bf3..ab5706e8ef 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index f563da3278..3673e252ae 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import type { UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { UsersRepository, UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; import { MessagingService } from '@/core/MessagingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['messaging'], diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index f61662af75..00e65b4875 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -3,7 +3,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { MessagingService } from '@/core/MessagingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 3b4507281f..5ead470314 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -6,7 +6,7 @@ import type { Muting } from '@/models/entities/Muting.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index 2fc5cee95e..612c4a4c04 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -4,7 +4,7 @@ import type { MutingsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 579466d4fd..d5caec6e1d 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -5,7 +5,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['clips', 'notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index ddfee31525..5ecf7cf458 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -5,7 +5,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 5765dfe66f..3c6e7bf768 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -5,7 +5,7 @@ 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 '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index edbd300e08..e742c1bb35 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NoteFavoritesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 8f4f2b2b9e..bb3a7c501a 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import type { NoteFavoritesRepository } from '@/models/index.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 515f03dcce..6b3b062c15 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -4,7 +4,7 @@ import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRe import type { IRemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { QueueService } from '@/core/QueueService.js'; import { PollService } from '@/core/PollService.js'; import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 2af734307d..839f893db2 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index 31ed962922..cf90d7b5f6 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,7 +1,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 97ef1a17ec..026a1baa3e 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -5,7 +5,7 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 7849cfa401..6b1b84a18e 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -4,7 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index b8ddf83f3c..140614d36e 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index 54e9b4939f..30016d48bc 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NoteThreadMutingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 7a3daf741e..ec16965998 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -9,7 +9,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 7378c4b600..74e459b426 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -5,7 +5,7 @@ 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 '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index a28229add3..90febdbce7 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -4,7 +4,7 @@ import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 22c49860bd..09f6acde9c 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -6,7 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 127f4ca65b..2e040c0601 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -3,7 +3,7 @@ import type { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvita import { IdService } from '@/core/IdService.js'; import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index 3474f22c6e..409006b0b0 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index e0b2b13c7f..3130d98ed1 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 7e54d33376..d2dd5731ee 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 06ea43d654..c3a1308286 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index d2c9616f68..aab32cc58c 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,8 +5,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '../../common/GetterService.js'; export const meta = { tags: ['users', 'notes'], 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 bb37dd2715..13badab727 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -8,7 +8,7 @@ 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 '../../common/GetterService.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['users'], -- cgit v1.2.3-freya